Merge pull request #8025 from scottbrady91/netcore/feature/users
Migrated back office to ASP.NET Core Identity
This commit is contained in:
@@ -33,7 +33,7 @@
|
||||
<dependency id="LightInject.Mvc" version="[2.0.0,2.999999)" />
|
||||
<dependency id="LightInject.WebApi" version="[2.0.0,2.999999)" />
|
||||
<dependency id="Markdown" version="[2.2.1,2.999999)" />
|
||||
<dependency id="Microsoft.AspNet.Identity.Owin" version="[2.2.2,2.999999)" />
|
||||
<dependency id="Microsoft.AspNet.Identity.Core" version="[2.2.2,2.999999)" />
|
||||
<dependency id="Microsoft.AspNet.Mvc" version="[5.2.7,5.999999)" />
|
||||
<dependency id="Microsoft.AspNet.SignalR.Core" version="[2.4.0,2.999999)" />
|
||||
<dependency id="Microsoft.AspNet.WebApi" version="[5.2.7,5.999999)" />
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Principal;
|
||||
|
||||
namespace Umbraco.Core
|
||||
{
|
||||
public static class ClaimsIdentityExtensions
|
||||
{
|
||||
public static string GetUserId(this IIdentity identity)
|
||||
{
|
||||
if (identity == null) throw new ArgumentNullException(nameof(identity));
|
||||
|
||||
string userId = null;
|
||||
if (identity is ClaimsIdentity claimsIdentity)
|
||||
{
|
||||
userId = claimsIdentity.FindFirstValue(ClaimTypes.NameIdentifier)
|
||||
?? claimsIdentity.FindFirstValue("sub");
|
||||
}
|
||||
|
||||
return userId;
|
||||
}
|
||||
|
||||
public static string GetUserName(this IIdentity identity)
|
||||
{
|
||||
if (identity == null) throw new ArgumentNullException(nameof(identity));
|
||||
|
||||
string username = null;
|
||||
if (identity is ClaimsIdentity claimsIdentity)
|
||||
{
|
||||
username = claimsIdentity.FindFirstValue(ClaimTypes.Name)
|
||||
?? claimsIdentity.FindFirstValue("preferred_username");
|
||||
}
|
||||
|
||||
return username;
|
||||
}
|
||||
|
||||
public static string FindFirstValue(this ClaimsIdentity identity, string claimType)
|
||||
{
|
||||
if (identity == null) throw new ArgumentNullException(nameof(identity));
|
||||
|
||||
return identity.FindFirst(claimType)?.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,16 @@
|
||||
/// The route name of the page shown when Umbraco has no published content.
|
||||
/// </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>
|
||||
public const string TwoFactorRememberBrowserCookie = "TwoFactorRememberBrowser";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Web;
|
||||
|
||||
namespace Umbraco.Tests.CoreThings
|
||||
{
|
||||
public class ClaimsIdentityExtensionsTests
|
||||
{
|
||||
[Test]
|
||||
public void FindFirstValue_WhenIdentityIsNull_ExpectArgumentNullException()
|
||||
{
|
||||
ClaimsIdentity identity = null;
|
||||
Assert.Throws<ArgumentNullException>(() => identity.FindFirstValue("test"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FindFirstValue_WhenClaimNotPresent_ExpectNull()
|
||||
{
|
||||
var identity = new ClaimsIdentity(new List<Claim>());
|
||||
var value = identity.FindFirstValue("test");
|
||||
Assert.IsNull(value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FindFirstValue_WhenMatchingClaimPresent_ExpectCorrectValue()
|
||||
{
|
||||
var expectedClaim = new Claim("test", "123", "string", "Umbraco");
|
||||
var identity = new ClaimsIdentity(new List<Claim> {expectedClaim});
|
||||
|
||||
var value = identity.FindFirstValue("test");
|
||||
|
||||
Assert.AreEqual(expectedClaim.Value, value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FindFirstValue_WhenMultipleMatchingClaimsPresent_ExpectFirstValue()
|
||||
{
|
||||
var expectedClaim = new Claim("test", "123", "string", "Umbraco");
|
||||
var dupeClaim = new Claim(expectedClaim.Type, Guid.NewGuid().ToString());
|
||||
var identity = new ClaimsIdentity(new List<Claim> {expectedClaim, dupeClaim});
|
||||
|
||||
var value = identity.FindFirstValue("test");
|
||||
|
||||
Assert.AreEqual(expectedClaim.Value, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Web.Security;
|
||||
|
||||
namespace Umbraco.Tests.Security
|
||||
{
|
||||
public class AuthenticationExtensionsTests
|
||||
{
|
||||
[Test]
|
||||
public void ToErrorMessage_When_Errors_Are_Null_Expect_ArgumentNullException()
|
||||
{
|
||||
IEnumerable<IdentityError> errors = null;
|
||||
|
||||
Assert.Throws<ArgumentNullException>(() => errors.ToErrorMessage());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ToErrorMessage_When_Single_Error_Expect_Error_Description()
|
||||
{
|
||||
const string expectedError = "invalid something";
|
||||
var errors = new List<IdentityError> {new IdentityError {Code = "1", Description = expectedError}};
|
||||
|
||||
var errorMessage = errors.ToErrorMessage();
|
||||
|
||||
Assert.AreEqual(expectedError, errorMessage);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ToErrorMessage_When_Multiple_Errors_Expect_Error_Descriptions_With_Comma_Delimiter()
|
||||
{
|
||||
const string error1 = "invalid something";
|
||||
const string error2 = "invalid something else";
|
||||
var errors = new List<IdentityError>
|
||||
{
|
||||
new IdentityError {Code = "1", Description = error1},
|
||||
new IdentityError {Code = "2", Description = error2}
|
||||
};
|
||||
|
||||
var errorMessage = errors.ToErrorMessage();
|
||||
|
||||
Assert.AreEqual($"{error1}, {error2}", errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Models.Membership;
|
||||
using Umbraco.Core.Security;
|
||||
using Umbraco.Web.Models.Identity;
|
||||
using Umbraco.Web.Security;
|
||||
|
||||
namespace Umbraco.Tests.Security
|
||||
{
|
||||
[TestFixture]
|
||||
public class BackOfficeClaimsPrincipalFactoryTests
|
||||
{
|
||||
private BackOfficeIdentityUser _testUser;
|
||||
private Mock<UserManager<BackOfficeIdentityUser>> _mockUserManager;
|
||||
|
||||
[Test]
|
||||
public void Ctor_When_UserManager_Is_Null_Expect_ArgumentNullException()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => new BackOfficeClaimsPrincipalFactory<BackOfficeIdentityUser>(
|
||||
null,
|
||||
new OptionsWrapper<IdentityOptions>(new IdentityOptions())));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Ctor_When_Options_Are_Null_Expect_ArgumentNullException()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => new BackOfficeClaimsPrincipalFactory<BackOfficeIdentityUser>(
|
||||
new Mock<UserManager<BackOfficeIdentityUser>>(new Mock<IUserStore<BackOfficeIdentityUser>>().Object,
|
||||
null, null, null, null, null, null, null, null).Object,
|
||||
null));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Ctor_When_Options_Value_Is_Null_Expect_ArgumentNullException()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => new BackOfficeClaimsPrincipalFactory<BackOfficeIdentityUser>(
|
||||
new Mock<UserManager<BackOfficeIdentityUser>>(new Mock<IUserStore<BackOfficeIdentityUser>>().Object,
|
||||
null, null, null, null, null, null, null, null).Object,
|
||||
new OptionsWrapper<IdentityOptions>(null)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateAsync_When_User_Is_Null_Expect_ArgumentNullException()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
|
||||
Assert.ThrowsAsync<ArgumentNullException>(async () => await sut.CreateAsync(null));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CreateAsync_Should_Create_Principal_With_Umbraco_Identity()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
|
||||
var claimsPrincipal = await sut.CreateAsync(_testUser);
|
||||
|
||||
var umbracoBackOfficeIdentity = claimsPrincipal.Identity as UmbracoBackOfficeIdentity;
|
||||
Assert.IsNotNull(umbracoBackOfficeIdentity);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CreateAsync_Should_Create_NameId()
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CreateAsync_When_SecurityStamp_Supported_Expect_SecurityStamp_Claim()
|
||||
{
|
||||
const string expectedClaimType = Constants.Web.SecurityStampClaimType;
|
||||
var expectedClaimValue = _testUser.SecurityStamp;
|
||||
|
||||
_mockUserManager.Setup(x => x.SupportsUserSecurityStamp).Returns(true);
|
||||
_mockUserManager.Setup(x => x.GetSecurityStampAsync(_testUser)).ReturnsAsync(_testUser.SecurityStamp);
|
||||
|
||||
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_When_Roles_Supported_Expect_Role_Claims_In_UmbracoIdentity()
|
||||
{
|
||||
const string expectedClaimType = ClaimTypes.Role;
|
||||
const string expectedClaimValue = "b87309fb-4caf-48dc-b45a-2b752d051508";
|
||||
|
||||
_testUser.Roles.Add(new Core.Models.Identity.IdentityUserRole<string>{RoleId = expectedClaimValue});
|
||||
_mockUserManager.Setup(x => x.SupportsUserRole).Returns(true);
|
||||
_mockUserManager.Setup(x => x.GetRolesAsync(_testUser)).ReturnsAsync(new[] {expectedClaimValue});
|
||||
|
||||
var sut = CreateSut();
|
||||
|
||||
var claimsPrincipal = await sut.CreateAsync(_testUser);
|
||||
|
||||
Assert.True(claimsPrincipal.HasClaim(expectedClaimType, expectedClaimValue));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CreateAsync_When_UserClaims_Supported_Expect_UserClaims_In_Actor()
|
||||
{
|
||||
const string expectedClaimType = "custom";
|
||||
const string expectedClaimValue = "val";
|
||||
|
||||
_testUser.Claims.Add(new Core.Models.Identity.IdentityUserClaim<int> {ClaimType = expectedClaimType, ClaimValue = expectedClaimValue});
|
||||
_mockUserManager.Setup(x => x.SupportsUserClaim).Returns(true);
|
||||
_mockUserManager.Setup(x => x.GetClaimsAsync(_testUser)).ReturnsAsync(
|
||||
new List<Claim> {new Claim(expectedClaimType, expectedClaimValue)});
|
||||
|
||||
var sut = CreateSut();
|
||||
|
||||
var claimsPrincipal = await sut.CreateAsync(_testUser);
|
||||
|
||||
Assert.True(claimsPrincipal.GetUmbracoIdentity().Actor.HasClaim(expectedClaimType, expectedClaimValue));
|
||||
}
|
||||
|
||||
[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 = "bob",
|
||||
Name = "Bob",
|
||||
Email = "bob@umbraco.test",
|
||||
SecurityStamp = "B6937738-9C17-4C7D-A25A-628A875F5177"
|
||||
};
|
||||
|
||||
_mockUserManager = new Mock<UserManager<BackOfficeIdentityUser>>(new Mock<IUserStore<BackOfficeIdentityUser>>().Object,
|
||||
null, null, null, null, null, null, null, null);
|
||||
_mockUserManager.Setup(x => x.GetUserIdAsync(_testUser)).ReturnsAsync(_testUser.Id.ToString);
|
||||
_mockUserManager.Setup(x => x.GetUserNameAsync(_testUser)).ReturnsAsync(_testUser.UserName);
|
||||
_mockUserManager.Setup(x => x.SupportsUserSecurityStamp).Returns(false);
|
||||
_mockUserManager.Setup(x => x.SupportsUserClaim).Returns(false);
|
||||
_mockUserManager.Setup(x => x.SupportsUserRole).Returns(false);
|
||||
}
|
||||
|
||||
private BackOfficeClaimsPrincipalFactory<BackOfficeIdentityUser> CreateSut()
|
||||
{
|
||||
return new BackOfficeClaimsPrincipalFactory<BackOfficeIdentityUser>(_mockUserManager.Object,
|
||||
new OptionsWrapper<IdentityOptions>(new IdentityOptions()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Owin.Security.DataProtection;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Models.Membership;
|
||||
using Umbraco.Net;
|
||||
using Umbraco.Web.Models.Identity;
|
||||
using Umbraco.Web.Security;
|
||||
|
||||
namespace Umbraco.Tests.Security
|
||||
{
|
||||
public class BackOfficeUserManagerTests
|
||||
{
|
||||
[Test]
|
||||
public async Task CheckPasswordAsync_When_Default_Password_Hasher_Validates_Umbraco7_Hash_Expect_Valid_Password()
|
||||
{
|
||||
const string v7Hash = "7Uob6fMTTxDIhWGebYiSxg==P+hgvWlXLbDd4cFLADn811KOaVI/9pg1PNvTuG5NklY=";
|
||||
const string plaintext = "4XxzH3s3&J";
|
||||
|
||||
var mockPasswordConfiguration = new Mock<IPasswordConfiguration>();
|
||||
var mockIpResolver = new Mock<IIpResolver>();
|
||||
var mockUserStore = new Mock<IUserPasswordStore<BackOfficeIdentityUser>>();
|
||||
var mockDataProtectionProvider = new Mock<IDataProtectionProvider>();
|
||||
|
||||
mockDataProtectionProvider.Setup(x => x.Create(It.IsAny<string>()))
|
||||
.Returns(new Mock<IDataProtector>().Object);
|
||||
mockPasswordConfiguration.Setup(x => x.HashAlgorithmType)
|
||||
.Returns("HMACSHA256");
|
||||
|
||||
var userManager = BackOfficeUserManager.Create(
|
||||
mockPasswordConfiguration.Object,
|
||||
mockIpResolver.Object,
|
||||
mockUserStore.Object,
|
||||
null,
|
||||
mockDataProtectionProvider.Object,
|
||||
new NullLogger<UserManager<BackOfficeIdentityUser>>());
|
||||
|
||||
var mockGlobalSettings = new Mock<IGlobalSettings>();
|
||||
mockGlobalSettings.Setup(x => x.DefaultUILanguage).Returns("test");
|
||||
|
||||
var user = new BackOfficeIdentityUser(mockGlobalSettings.Object, 2, new List<IReadOnlyUserGroup>())
|
||||
{
|
||||
UserName = "alice",
|
||||
Name = "Alice",
|
||||
Email = "alice@umbraco.test",
|
||||
PasswordHash = v7Hash
|
||||
};
|
||||
|
||||
mockUserStore.Setup(x => x.GetPasswordHashAsync(user, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(v7Hash);
|
||||
|
||||
var isValidPassword = await userManager.CheckPasswordAsync(user, plaintext);
|
||||
|
||||
Assert.True(isValidPassword);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Web.Security;
|
||||
|
||||
namespace Umbraco.Tests.Security
|
||||
{
|
||||
public class NopLookupNormalizerTests
|
||||
{
|
||||
[Test]
|
||||
[TestCase(null)]
|
||||
[TestCase("")]
|
||||
[TestCase(" ")]
|
||||
public void NormalizeName_When_Name_Null_Or_Whitespace_Expect_Same_Returned(string name)
|
||||
{
|
||||
var sut = new NopLookupNormalizer();
|
||||
|
||||
var normalizedName = sut.NormalizeName(name);
|
||||
|
||||
Assert.AreEqual(name, normalizedName);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NormalizeName_Expect_Input_Returned()
|
||||
{
|
||||
var name = Guid.NewGuid().ToString();
|
||||
var sut = new NopLookupNormalizer();
|
||||
|
||||
var normalizedName = sut.NormalizeName(name);
|
||||
|
||||
Assert.AreEqual(name, normalizedName);
|
||||
}
|
||||
[Test]
|
||||
[TestCase(null)]
|
||||
[TestCase("")]
|
||||
[TestCase(" ")]
|
||||
public void NormalizeEmail_When_Name_Null_Or_Whitespace_Expect_Same_Returned(string email)
|
||||
{
|
||||
var sut = new NopLookupNormalizer();
|
||||
|
||||
var normalizedEmail = sut.NormalizeEmail(email);
|
||||
|
||||
Assert.AreEqual(email, normalizedEmail);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NormalizeEmail_Expect_Input_Returned()
|
||||
{
|
||||
var email = $"{Guid.NewGuid()}@umbraco";
|
||||
var sut = new NopLookupNormalizer();
|
||||
|
||||
var normalizedEmail = sut.NormalizeEmail(email);
|
||||
|
||||
Assert.AreEqual(email, normalizedEmail);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Owin.Security.DataProtection;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Models.Membership;
|
||||
using Umbraco.Web.Models.Identity;
|
||||
using Umbraco.Web.Security;
|
||||
|
||||
namespace Umbraco.Tests.Security
|
||||
{
|
||||
public class OwinDataProtectorTokenProviderTests
|
||||
{
|
||||
private Mock<IDataProtector> _mockDataProtector;
|
||||
private Mock<UserManager<BackOfficeIdentityUser>> _mockUserManager;
|
||||
private BackOfficeIdentityUser _testUser;
|
||||
private const string _testPurpose = "test";
|
||||
|
||||
[Test]
|
||||
public void Ctor_When_Protector_Is_Null_Expect_ArgumentNullException()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => new OwinDataProtectorTokenProvider<BackOfficeIdentityUser>(null));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CanGenerateTwoFactorTokenAsync_Expect_False()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
|
||||
var canGenerate = await sut.CanGenerateTwoFactorTokenAsync(_mockUserManager.Object, _testUser);
|
||||
|
||||
Assert.False(canGenerate);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GenerateAsync_When_UserManager_Is_Null_Expect_ArgumentNullException()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
Assert.ThrowsAsync<ArgumentNullException>(async () => await sut.GenerateAsync(null, null, _testUser));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GenerateAsync_When_User_Is_Null_Expect_ArgumentNullException()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
Assert.ThrowsAsync<ArgumentNullException>(async () => await sut.GenerateAsync(null, _mockUserManager.Object, null));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GenerateAsync_When_Token_Generated_Expect_Ticks_And_User_ID()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
|
||||
var token = await sut.GenerateAsync(null, _mockUserManager.Object, _testUser);
|
||||
|
||||
using (var reader = new BinaryReader(new MemoryStream(Convert.FromBase64String(token))))
|
||||
{
|
||||
var creationTime = new DateTimeOffset(reader.ReadInt64(), TimeSpan.Zero);
|
||||
var foundUserId = reader.ReadString();
|
||||
|
||||
Assert.That(creationTime.DateTime, Is.EqualTo(DateTime.UtcNow).Within(1).Minutes);
|
||||
Assert.AreEqual(_testUser.Id.ToString(), foundUserId);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GenerateAsync_When_Token_Generated_With_Purpose_Expect_Purpose_In_Token()
|
||||
{
|
||||
var expectedPurpose = Guid.NewGuid().ToString();
|
||||
|
||||
var sut = CreateSut();
|
||||
|
||||
var token = await sut.GenerateAsync(expectedPurpose, _mockUserManager.Object, _testUser);
|
||||
|
||||
using (var reader = new BinaryReader(new MemoryStream(Convert.FromBase64String(token))))
|
||||
{
|
||||
reader.ReadInt64(); // creation time
|
||||
reader.ReadString(); // user ID
|
||||
var purpose = reader.ReadString();
|
||||
|
||||
Assert.AreEqual(expectedPurpose, purpose);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GenerateAsync_When_Token_Generated_And_SecurityStamp_Supported_Expect_SecurityStamp_In_Token()
|
||||
{
|
||||
var expectedSecurityStamp = Guid.NewGuid().ToString();
|
||||
|
||||
var sut = CreateSut();
|
||||
_mockUserManager.Setup(x => x.SupportsUserSecurityStamp).Returns(true);
|
||||
_mockUserManager.Setup(x => x.GetSecurityStampAsync(_testUser)).ReturnsAsync(expectedSecurityStamp);
|
||||
|
||||
var token = await sut.GenerateAsync(null, _mockUserManager.Object, _testUser);
|
||||
|
||||
using (var reader = new BinaryReader(new MemoryStream(Convert.FromBase64String(token))))
|
||||
{
|
||||
reader.ReadInt64(); // creation time
|
||||
reader.ReadString(); // user ID
|
||||
reader.ReadString(); // purpose
|
||||
var securityStamp = reader.ReadString();
|
||||
|
||||
Assert.AreEqual(expectedSecurityStamp, securityStamp);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase(null)]
|
||||
[TestCase("")]
|
||||
[TestCase(" ")]
|
||||
public void ValidateAsync_When_Token_Is_Null_Or_Whitespace_Expect_ArgumentNullException(string token)
|
||||
{
|
||||
var sut = CreateSut();
|
||||
|
||||
Assert.ThrowsAsync<ArgumentNullException>(() => sut.ValidateAsync(null, token, _mockUserManager.Object, _testUser));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ValidateAsync_When_UserManager_Is_Null_Expect_ArgumentNullException()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
|
||||
Assert.ThrowsAsync<ArgumentNullException>(() => sut.ValidateAsync(null, Guid.NewGuid().ToString(), null, _testUser));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ValidateAsync_When_User_Is_Null_Expect_ArgumentNullException()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
|
||||
Assert.ThrowsAsync<ArgumentNullException>(() => sut.ValidateAsync(null, Guid.NewGuid().ToString(), _mockUserManager.Object, null));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ValidateAsync_When_Token_Has_Expired_Expect_False()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
var testToken = CreateTestToken(creationDate: DateTime.UtcNow.AddYears(-10));
|
||||
|
||||
var isValid = await sut.ValidateAsync(null, testToken, _mockUserManager.Object, _testUser);
|
||||
|
||||
Assert.False(isValid);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ValidateAsync_When_Token_Was_Issued_To_Wrong_User_Expect_False()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
var testToken = CreateTestToken(userId: Guid.NewGuid().ToString());
|
||||
|
||||
var isValid = await sut.ValidateAsync(_testPurpose, testToken, _mockUserManager.Object, _testUser);
|
||||
|
||||
Assert.False(isValid);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ValidateAsync_When_Token_Was_Has_Wrong_Purpose_Expect_False()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
var testToken = CreateTestToken(purpose: "invalid");
|
||||
|
||||
var isValid = await sut.ValidateAsync("valid", testToken, _mockUserManager.Object, _testUser);
|
||||
|
||||
Assert.False(isValid);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ValidateAsync_When_Token_Was_Has_Wrong_SecurityStamp_Expect_False()
|
||||
{
|
||||
var sut = CreateSut();
|
||||
_mockUserManager.Setup(x => x.SupportsUserSecurityStamp).Returns(true);
|
||||
_mockUserManager.Setup(x => x.GetSecurityStampAsync(_testUser)).ReturnsAsync(Guid.NewGuid().ToString);
|
||||
|
||||
var testToken = CreateTestToken(securityStamp: "invalid");
|
||||
|
||||
var isValid = await sut.ValidateAsync(_testPurpose, testToken, _mockUserManager.Object, _testUser);
|
||||
|
||||
Assert.False(isValid);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ValidateAsync_When_Valid_Token_Expect_True()
|
||||
{
|
||||
const string validPurpose = "test";
|
||||
var validSecurityStamp = Guid.NewGuid().ToString();
|
||||
|
||||
var sut = CreateSut();
|
||||
_mockUserManager.Setup(x => x.SupportsUserSecurityStamp).Returns(true);
|
||||
_mockUserManager.Setup(x => x.GetSecurityStampAsync(_testUser)).ReturnsAsync(validSecurityStamp);
|
||||
|
||||
var testToken = CreateTestToken(
|
||||
creationDate: DateTime.UtcNow,
|
||||
userId: _testUser.Id.ToString(),
|
||||
purpose: validPurpose,
|
||||
securityStamp: validSecurityStamp);
|
||||
|
||||
var isValid = await sut.ValidateAsync(validPurpose, testToken, _mockUserManager.Object, _testUser);
|
||||
|
||||
Assert.True(isValid);
|
||||
}
|
||||
|
||||
private OwinDataProtectorTokenProvider<BackOfficeIdentityUser> CreateSut()
|
||||
=> new OwinDataProtectorTokenProvider<BackOfficeIdentityUser>(_mockDataProtector.Object);
|
||||
|
||||
private string CreateTestToken(DateTime? creationDate = null, string userId = null, string purpose = null, string securityStamp = null)
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
using (var writer = new BinaryWriter(ms))
|
||||
{
|
||||
writer.Write(creationDate?.Ticks ?? DateTimeOffset.UtcNow.UtcTicks);
|
||||
writer.Write(userId ?? _testUser.Id.ToString());
|
||||
writer.Write(purpose ?? _testPurpose);
|
||||
writer.Write(securityStamp ?? "");
|
||||
}
|
||||
|
||||
return Convert.ToBase64String(ms.ToArray());
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_mockDataProtector = new Mock<IDataProtector>();
|
||||
_mockDataProtector.Setup(x => x.Protect(It.IsAny<byte[]>())).Returns((byte[] originalBytes) => originalBytes);
|
||||
_mockDataProtector.Setup(x => x.Unprotect(It.IsAny<byte[]>())).Returns((byte[] originalBytes) => originalBytes);
|
||||
|
||||
var mockGlobalSettings = new Mock<IGlobalSettings>();
|
||||
mockGlobalSettings.Setup(x => x.DefaultUILanguage).Returns("test");
|
||||
|
||||
_mockUserManager = new Mock<UserManager<BackOfficeIdentityUser>>(new Mock<IUserStore<BackOfficeIdentityUser>>().Object,
|
||||
null, null, null, null, null, null, null, null);
|
||||
_mockUserManager.Setup(x => x.SupportsUserSecurityStamp).Returns(false);
|
||||
|
||||
_testUser = new BackOfficeIdentityUser(mockGlobalSettings.Object, 2, new List<IReadOnlyUserGroup>())
|
||||
{
|
||||
UserName = "alice",
|
||||
Name = "Alice",
|
||||
Email = "alice@umbraco.test",
|
||||
SecurityStamp = Guid.NewGuid().ToString()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ namespace Umbraco.Tests.Security
|
||||
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(Microsoft.AspNet.Identity.Constants.DefaultSecurityStampClaimType, securityStamp, ClaimValueTypes.String, TestIssuer, TestIssuer),
|
||||
new Claim(Constants.Web.SecurityStampClaimType, securityStamp, ClaimValueTypes.String, TestIssuer, TestIssuer),
|
||||
});
|
||||
|
||||
var backofficeIdentity = UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity);
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
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.Configuration;
|
||||
using Umbraco.Core.Models.Membership;
|
||||
using Umbraco.Net;
|
||||
using Umbraco.Web.Models.Identity;
|
||||
using Umbraco.Web.Security;
|
||||
|
||||
namespace Umbraco.Tests.Security
|
||||
{
|
||||
public class UmbracoSecurityStampValidatorTests
|
||||
{
|
||||
private Mock<IOwinContext> _mockOwinContext;
|
||||
private Mock<BackOfficeUserManager<BackOfficeIdentityUser>> _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, BackOfficeUserManager<BackOfficeIdentityUser>, BackOfficeIdentityUser>(
|
||||
TimeSpan.MaxValue, null, null));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task OnValidateIdentity_When_Validation_Interval_Not_Met_Expect_No_Op()
|
||||
{
|
||||
var func = UmbracoSecurityStampValidator
|
||||
.OnValidateIdentity<BackOfficeSignInManager, BackOfficeUserManager<BackOfficeIdentityUser>, 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, BackOfficeUserManager<BackOfficeIdentityUser>, BackOfficeIdentityUser>(
|
||||
TimeSpan.MinValue, null, identity => throw new Exception());
|
||||
|
||||
_mockOwinContext.Setup(x => x.Get<BackOfficeUserManager<BackOfficeIdentityUser>>(It.IsAny<string>()))
|
||||
.Returns((BackOfficeUserManager<BackOfficeIdentityUser>) 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, BackOfficeUserManager<BackOfficeIdentityUser>, 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, BackOfficeUserManager<BackOfficeIdentityUser>, 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, BackOfficeUserManager<BackOfficeIdentityUser>, 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, BackOfficeUserManager<BackOfficeIdentityUser>, 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, BackOfficeUserManager<BackOfficeIdentityUser>, 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, BackOfficeUserManager<BackOfficeIdentityUser>, BackOfficeIdentityUser, Task<ClaimsIdentity>> regenFunc =
|
||||
(signInManager, userManager, user) =>
|
||||
{
|
||||
regenFuncCalled = true;
|
||||
return Task.FromResult(expectedIdentity);
|
||||
};
|
||||
|
||||
var func = UmbracoSecurityStampValidator
|
||||
.OnValidateIdentity<BackOfficeSignInManager, BackOfficeUserManager<BackOfficeIdentityUser>, 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<BackOfficeUserManager<BackOfficeIdentityUser>>(
|
||||
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<BackOfficeUserManager<BackOfficeIdentityUser>>(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>()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,6 +124,7 @@
|
||||
<Compile Include="Configurations\GlobalSettingsTests.cs" />
|
||||
<Compile Include="CoreThings\CallContextTests.cs" />
|
||||
<Compile Include="Components\ComponentTests.cs" />
|
||||
<Compile Include="CoreThings\ClaimsIdentityExtensionsTests.cs" />
|
||||
<Compile Include="CoreThings\EnumExtensionsTests.cs" />
|
||||
<Compile Include="CoreThings\GuidUtilsTests.cs" />
|
||||
<Compile Include="CoreThings\HexEncoderTests.cs" />
|
||||
@@ -145,7 +146,13 @@
|
||||
<Compile Include="Persistence\Mappers\MapperTestBase.cs" />
|
||||
<Compile Include="Persistence\Repositories\DocumentRepositoryTest.cs" />
|
||||
<Compile Include="Persistence\Repositories\EntityRepositoryTest.cs" />
|
||||
<Compile Include="Security\AuthenticationExtensionsTests.cs" />
|
||||
<Compile Include="Security\BackOfficeClaimsPrincipalFactoryTests.cs" />
|
||||
<Compile Include="Persistence\Repositories\KeyValueRepositoryTests.cs" />
|
||||
<Compile Include="Security\BackOfficeUserManagerTests.cs" />
|
||||
<Compile Include="Security\NopLookupNormalizerTests.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" />
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Formatting;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web.Http;
|
||||
using System.Web.Http.Controllers;
|
||||
using System.Web.Http.Hosting;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Owin;
|
||||
using Moq;
|
||||
using Newtonsoft.Json;
|
||||
using NUnit.Framework;
|
||||
@@ -19,12 +26,10 @@ using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.Membership;
|
||||
using Umbraco.Core.Persistence;
|
||||
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
|
||||
using Umbraco.Core.Persistence.Mappers;
|
||||
using Umbraco.Core.Persistence.Querying;
|
||||
using Umbraco.Core.Persistence.SqlSyntax;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Core.Strings;
|
||||
using Umbraco.Tests.TestHelpers;
|
||||
using Umbraco.Tests.TestHelpers.ControllerTesting;
|
||||
using Umbraco.Tests.TestHelpers.Entities;
|
||||
@@ -39,6 +44,9 @@ using Umbraco.Core.Configuration.UmbracoSettings;
|
||||
using Umbraco.Core.Hosting;
|
||||
using Umbraco.Web.Routing;
|
||||
using Umbraco.Core.Media;
|
||||
using Umbraco.Net;
|
||||
using Umbraco.Web.Models.Identity;
|
||||
using Umbraco.Web.Security;
|
||||
|
||||
namespace Umbraco.Tests.Web.Controllers
|
||||
{
|
||||
@@ -62,7 +70,7 @@ namespace Umbraco.Tests.Web.Controllers
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async System.Threading.Tasks.Task Save_User()
|
||||
public async Task Save_User()
|
||||
{
|
||||
ApiController CtrlFactory(HttpRequestMessage message, IUmbracoContextAccessor umbracoContextAccessor)
|
||||
{
|
||||
@@ -149,7 +157,7 @@ namespace Umbraco.Tests.Web.Controllers
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async System.Threading.Tasks.Task GetPagedUsers_Empty()
|
||||
public async Task GetPagedUsers_Empty()
|
||||
{
|
||||
ApiController CtrlFactory(HttpRequestMessage message, IUmbracoContextAccessor umbracoContextAccessor)
|
||||
{
|
||||
@@ -183,7 +191,7 @@ namespace Umbraco.Tests.Web.Controllers
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async System.Threading.Tasks.Task GetPagedUsers_10()
|
||||
public async Task GetPagedUsers_10()
|
||||
{
|
||||
ApiController CtrlFactory(HttpRequestMessage message, IUmbracoContextAccessor umbracoContextAccessor)
|
||||
{
|
||||
@@ -227,7 +235,7 @@ namespace Umbraco.Tests.Web.Controllers
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async System.Threading.Tasks.Task GetPagedUsers_Fips()
|
||||
public async Task GetPagedUsers_Fips()
|
||||
{
|
||||
await RunFipsTest("GetPagedUsers", mock =>
|
||||
{
|
||||
@@ -246,7 +254,7 @@ namespace Umbraco.Tests.Web.Controllers
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async System.Threading.Tasks.Task GetById_Fips()
|
||||
public async Task GetById_Fips()
|
||||
{
|
||||
const int mockUserId = 1234;
|
||||
var user = MockedUser.CreateUser();
|
||||
@@ -264,7 +272,7 @@ namespace Umbraco.Tests.Web.Controllers
|
||||
}
|
||||
|
||||
|
||||
private async System.Threading.Tasks.Task RunFipsTest(string action, Action<Mock<IUserService>> userServiceSetup,
|
||||
private async Task RunFipsTest(string action, Action<Mock<IUserService>> userServiceSetup,
|
||||
Action<Tuple<HttpResponseMessage, string>> verification,
|
||||
object routeDefaults = null, string url = null)
|
||||
{
|
||||
@@ -324,5 +332,185 @@ namespace Umbraco.Tests.Web.Controllers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task PostUnlockUsers_When_UserIds_Not_Supplied_Expect_Ok_Response()
|
||||
{
|
||||
var usersController = CreateSut();
|
||||
|
||||
usersController.Request = new HttpRequestMessage();
|
||||
|
||||
var response = await usersController.PostUnlockUsers(new int[0]);
|
||||
|
||||
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PostUnlockUsers_When_User_Does_Not_Exist_Expect_InvalidOperationException()
|
||||
{
|
||||
var mockUserManager = CreateMockUserManager();
|
||||
var usersController = CreateSut(mockUserManager);
|
||||
|
||||
mockUserManager.Setup(x => x.FindByIdAsync(It.IsAny<string>()))
|
||||
.ReturnsAsync((BackOfficeIdentityUser) null);
|
||||
|
||||
Assert.ThrowsAsync<InvalidOperationException>(async () => await usersController.PostUnlockUsers(new[] {1}));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task PostUnlockUsers_When_User_Lockout_Update_Fails_Expect_Failure_Response()
|
||||
{
|
||||
var mockUserManager = CreateMockUserManager();
|
||||
var usersController = CreateSut(mockUserManager);
|
||||
|
||||
const string expectedMessage = "identity error!";
|
||||
var user = new BackOfficeIdentityUser(
|
||||
new Mock<IGlobalSettings>().Object,
|
||||
1,
|
||||
new List<IReadOnlyUserGroup>())
|
||||
{
|
||||
Name = "bob"
|
||||
};
|
||||
|
||||
mockUserManager.Setup(x => x.FindByIdAsync(It.IsAny<string>()))
|
||||
.ReturnsAsync(user);
|
||||
mockUserManager.Setup(x => x.SetLockoutEndDateAsync(user, It.IsAny<DateTimeOffset?>()))
|
||||
.ReturnsAsync(IdentityResult.Failed(new IdentityError {Description = expectedMessage}));
|
||||
|
||||
var response = await usersController.PostUnlockUsers(new[] { 1 });
|
||||
|
||||
Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
Assert.True(response.Headers.TryGetValues("X-Status-Reason", out var values));
|
||||
Assert.True(values.Contains("Validation failed"));
|
||||
|
||||
var responseContent = response.Content as ObjectContent<HttpError>;
|
||||
var responseValue = responseContent?.Value as HttpError;
|
||||
Assert.NotNull(responseValue);
|
||||
Assert.True(responseValue.Message.Contains(expectedMessage));
|
||||
Assert.True(responseValue.Message.Contains(user.Id.ToString()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task PostUnlockUsers_When_One_UserId_Supplied_Expect_User_Locked_Out_With_Correct_Response_Message()
|
||||
{
|
||||
var mockUserManager = CreateMockUserManager();
|
||||
var usersController = CreateSut(mockUserManager);
|
||||
|
||||
var user = new BackOfficeIdentityUser(
|
||||
new Mock<IGlobalSettings>().Object,
|
||||
1,
|
||||
new List<IReadOnlyUserGroup>())
|
||||
{
|
||||
Name = "bob"
|
||||
};
|
||||
|
||||
mockUserManager.Setup(x => x.FindByIdAsync(user.Id.ToString()))
|
||||
.ReturnsAsync(user);
|
||||
mockUserManager.Setup(x => x.SetLockoutEndDateAsync(user, It.IsAny<DateTimeOffset>()))
|
||||
.ReturnsAsync(IdentityResult.Success)
|
||||
.Verifiable();
|
||||
|
||||
var response = await usersController.PostUnlockUsers(new[] { user.Id });
|
||||
|
||||
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var responseContent = response.Content as ObjectContent<SimpleNotificationModel>;
|
||||
var notifications = responseContent?.Value as SimpleNotificationModel;
|
||||
Assert.NotNull(notifications);
|
||||
Assert.AreEqual(user.Name, notifications.Message);
|
||||
mockUserManager.Verify();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task PostUnlockUsers_When_Multiple_UserIds_Supplied_Expect_User_Locked_Out_With_Correct_Response_Message()
|
||||
{
|
||||
var mockUserManager = CreateMockUserManager();
|
||||
var usersController = CreateSut(mockUserManager);
|
||||
|
||||
var user1 = new BackOfficeIdentityUser(
|
||||
new Mock<IGlobalSettings>().Object,
|
||||
1,
|
||||
new List<IReadOnlyUserGroup>())
|
||||
{
|
||||
Name = "bob"
|
||||
};
|
||||
var user2 = new BackOfficeIdentityUser(
|
||||
new Mock<IGlobalSettings>().Object,
|
||||
2,
|
||||
new List<IReadOnlyUserGroup>())
|
||||
{
|
||||
Name = "alice"
|
||||
};
|
||||
var userIdsToLock = new[] {user1.Id, user2.Id};
|
||||
|
||||
mockUserManager.Setup(x => x.FindByIdAsync(user1.Id.ToString()))
|
||||
.ReturnsAsync(user1);
|
||||
mockUserManager.Setup(x => x.FindByIdAsync(user2.Id.ToString()))
|
||||
.ReturnsAsync(user2);
|
||||
mockUserManager.Setup(x => x.SetLockoutEndDateAsync(user1, It.IsAny<DateTimeOffset>()))
|
||||
.ReturnsAsync(IdentityResult.Success)
|
||||
.Verifiable();
|
||||
mockUserManager.Setup(x => x.SetLockoutEndDateAsync(user2, It.IsAny<DateTimeOffset>()))
|
||||
.ReturnsAsync(IdentityResult.Success)
|
||||
.Verifiable();
|
||||
|
||||
var response = await usersController.PostUnlockUsers(userIdsToLock);
|
||||
|
||||
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var responseContent = response.Content as ObjectContent<SimpleNotificationModel>;
|
||||
var notifications = responseContent?.Value as SimpleNotificationModel;
|
||||
Assert.NotNull(notifications);
|
||||
Assert.AreEqual(userIdsToLock.Length.ToString(), notifications.Message);
|
||||
mockUserManager.Verify();
|
||||
}
|
||||
|
||||
private UsersController CreateSut(IMock<BackOfficeUserManager<BackOfficeIdentityUser>> mockUserManager = null)
|
||||
{
|
||||
var mockLocalizedTextService = new Mock<ILocalizedTextService>();
|
||||
mockLocalizedTextService.Setup(x => x.Localize(It.IsAny<string>(), It.IsAny<CultureInfo>(), It.IsAny<IDictionary<string, string>>()))
|
||||
.Returns((string key, CultureInfo ci, IDictionary<string, string> tokens)
|
||||
=> tokens.Aggregate("", (current, next) => current + (current == string.Empty ? "" : ",") + next.Value));
|
||||
|
||||
var usersController = new UsersController(
|
||||
Factory.GetInstance<IGlobalSettings>(),
|
||||
Factory.GetInstance<IUmbracoContextAccessor>(),
|
||||
Factory.GetInstance<ISqlContext>(),
|
||||
ServiceContext.CreatePartial(localizedTextService: mockLocalizedTextService.Object),
|
||||
Factory.GetInstance<AppCaches>(),
|
||||
Factory.GetInstance<IProfilingLogger>(),
|
||||
Factory.GetInstance<IRuntimeState>(),
|
||||
Factory.GetInstance<IMediaFileSystem>(),
|
||||
ShortStringHelper,
|
||||
Factory.GetInstance<UmbracoMapper>(),
|
||||
Factory.GetInstance<IContentSettings>(),
|
||||
Factory.GetInstance<IHostingEnvironment>(),
|
||||
Factory.GetInstance<IImageUrlGenerator>(),
|
||||
Factory.GetInstance<IPublishedUrlProvider>(),
|
||||
Factory.GetInstance<ISecuritySettings>());
|
||||
|
||||
var mockOwinContext = new Mock<IOwinContext>();
|
||||
var mockUserManagerMarker = new Mock<IBackOfficeUserManagerMarker>();
|
||||
|
||||
mockOwinContext.Setup(x => x.Get<IBackOfficeUserManagerMarker>(It.IsAny<string>()))
|
||||
.Returns(mockUserManagerMarker.Object);
|
||||
mockUserManagerMarker.Setup(x => x.GetManager(It.IsAny<IOwinContext>()))
|
||||
.Returns(mockUserManager?.Object ?? CreateMockUserManager().Object);
|
||||
|
||||
usersController.Request = new HttpRequestMessage();
|
||||
usersController.Request.Properties["MS_OwinContext"] = mockOwinContext.Object;
|
||||
usersController.Request.Properties[HttpPropertyKeys.RequestContextKey] = new HttpRequestContext {Configuration = new HttpConfiguration()};
|
||||
|
||||
return usersController;
|
||||
}
|
||||
|
||||
private static Mock<BackOfficeUserManager<BackOfficeIdentityUser>> CreateMockUserManager()
|
||||
{
|
||||
return new Mock<BackOfficeUserManager<BackOfficeIdentityUser>>(
|
||||
new Mock<IPasswordConfiguration>().Object,
|
||||
new Mock<IIpResolver>().Object,
|
||||
new Mock<IUserStore<BackOfficeIdentityUser>>().Object,
|
||||
null, null, null, null, null, null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,6 @@
|
||||
<PackageReference Include="ClientDependency-Mvc5" Version="1.9.3" />
|
||||
<PackageReference Include="ImageProcessor.Web" Version="4.10.0.100" />
|
||||
<PackageReference Include="ImageProcessor.Web.Config" Version="2.5.0.100" />
|
||||
<PackageReference Include="Microsoft.AspNet.Identity.Owin" Version="2.2.2" />
|
||||
<PackageReference Include="Microsoft.AspNet.Mvc" Version="5.2.7" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi" Version="5.2.7" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="2.10.0" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
@@ -8,8 +8,7 @@ using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using System.Web.Http;
|
||||
using System.Web.Mvc;
|
||||
using Microsoft.AspNet.Identity;
|
||||
using Microsoft.AspNet.Identity.Owin;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Cache;
|
||||
using Umbraco.Core.Models;
|
||||
@@ -105,15 +104,15 @@ namespace Umbraco.Web.Editors
|
||||
if (decoded.IsNullOrWhiteSpace())
|
||||
throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound));
|
||||
|
||||
var identityUser = await UserManager.FindByIdAsync(id);
|
||||
var identityUser = await UserManager.FindByIdAsync(id.ToString());
|
||||
if (identityUser == null)
|
||||
throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound));
|
||||
|
||||
var result = await UserManager.ConfirmEmailAsync(id, decoded);
|
||||
var result = await UserManager.ConfirmEmailAsync(identityUser, decoded);
|
||||
|
||||
if (result.Succeeded == false)
|
||||
{
|
||||
throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse(string.Join(", ", result.Errors)));
|
||||
throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse(result.Errors.ToErrorMessage()));
|
||||
}
|
||||
|
||||
Request.TryGetOwinContext().Result.Authentication.SignOut(
|
||||
@@ -131,13 +130,16 @@ namespace Umbraco.Web.Editors
|
||||
[ValidateAngularAntiForgeryToken]
|
||||
public async Task<HttpResponseMessage> PostUnLinkLogin(UnLinkLoginModel unlinkLoginModel)
|
||||
{
|
||||
var user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
|
||||
if (user == null) throw new InvalidOperationException("Could not find user");
|
||||
|
||||
var result = await UserManager.RemoveLoginAsync(
|
||||
User.Identity.GetUserId<int>(),
|
||||
new UserLoginInfo(unlinkLoginModel.LoginProvider, unlinkLoginModel.ProviderKey));
|
||||
user,
|
||||
unlinkLoginModel.LoginProvider,
|
||||
unlinkLoginModel.ProviderKey);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
var user = await UserManager.FindByIdAsync(User.Identity.GetUserId<int>());
|
||||
await SignInManager.SignInAsync(user, isPersistent: true, rememberBrowser: false);
|
||||
return Request.CreateResponse(HttpStatusCode.OK);
|
||||
}
|
||||
@@ -225,7 +227,7 @@ namespace Umbraco.Web.Editors
|
||||
[ValidateAngularAntiForgeryToken]
|
||||
public async Task<Dictionary<string, string>> GetCurrentUserLinkedLogins()
|
||||
{
|
||||
var identityUser = await UserManager.FindByIdAsync(UmbracoContext.Security.GetUserId().ResultOr(0));
|
||||
var identityUser = await UserManager.FindByIdAsync(UmbracoContext.Security.GetUserId().ResultOr(0).ToString());
|
||||
return identityUser.Logins.ToDictionary(x => x.LoginProvider, x => x.ProviderKey);
|
||||
}
|
||||
|
||||
@@ -244,61 +246,58 @@ namespace Umbraco.Web.Editors
|
||||
var result = await SignInManager.PasswordSignInAsync(
|
||||
loginModel.Username, loginModel.Password, isPersistent: true, shouldLockout: true);
|
||||
|
||||
switch (result)
|
||||
if (result.Succeeded)
|
||||
{
|
||||
case SignInStatus.Success:
|
||||
// get the user
|
||||
var user = Services.UserService.GetByUsername(loginModel.Username);
|
||||
UserManager.RaiseLoginSuccessEvent(user.Id);
|
||||
|
||||
// get the user
|
||||
var user = Services.UserService.GetByUsername(loginModel.Username);
|
||||
UserManager.RaiseLoginSuccessEvent(user.Id);
|
||||
|
||||
return SetPrincipalAndReturnUserDetail(user, owinContext.Request.User);
|
||||
case SignInStatus.RequiresVerification:
|
||||
|
||||
var twofactorOptions = UserManager as IUmbracoBackOfficeTwoFactorOptions;
|
||||
if (twofactorOptions == null)
|
||||
{
|
||||
throw new HttpResponseException(
|
||||
Request.CreateErrorResponse(
|
||||
HttpStatusCode.BadRequest,
|
||||
"UserManager does not implement " + typeof(IUmbracoBackOfficeTwoFactorOptions)));
|
||||
}
|
||||
|
||||
var twofactorView = twofactorOptions.GetTwoFactorView(
|
||||
owinContext,
|
||||
UmbracoContext,
|
||||
loginModel.Username);
|
||||
|
||||
if (twofactorView.IsNullOrWhiteSpace())
|
||||
{
|
||||
throw new HttpResponseException(
|
||||
Request.CreateErrorResponse(
|
||||
HttpStatusCode.BadRequest,
|
||||
typeof(IUmbracoBackOfficeTwoFactorOptions) + ".GetTwoFactorView returned an empty string"));
|
||||
}
|
||||
|
||||
var attemptedUser = Services.UserService.GetByUsername(loginModel.Username);
|
||||
|
||||
// create a with information to display a custom two factor send code view
|
||||
var verifyResponse = Request.CreateResponse(HttpStatusCode.PaymentRequired, new
|
||||
{
|
||||
twoFactorView = twofactorView,
|
||||
userId = attemptedUser.Id
|
||||
});
|
||||
|
||||
UserManager.RaiseLoginRequiresVerificationEvent(attemptedUser.Id);
|
||||
|
||||
return verifyResponse;
|
||||
|
||||
case SignInStatus.LockedOut:
|
||||
case SignInStatus.Failure:
|
||||
default:
|
||||
// return BadRequest (400), we don't want to return a 401 because that get's intercepted
|
||||
// by our angular helper because it thinks that we need to re-perform the request once we are
|
||||
// authorized and we don't want to return a 403 because angular will show a warning message indicating
|
||||
// that the user doesn't have access to perform this function, we just want to return a normal invalid message.
|
||||
throw new HttpResponseException(HttpStatusCode.BadRequest);
|
||||
return SetPrincipalAndReturnUserDetail(user, owinContext.Request.User);
|
||||
}
|
||||
|
||||
if (result.RequiresTwoFactor)
|
||||
{
|
||||
var twofactorOptions = UserManager as IUmbracoBackOfficeTwoFactorOptions;
|
||||
if (twofactorOptions == null)
|
||||
{
|
||||
throw new HttpResponseException(
|
||||
Request.CreateErrorResponse(
|
||||
HttpStatusCode.BadRequest,
|
||||
"UserManager does not implement " + typeof(IUmbracoBackOfficeTwoFactorOptions)));
|
||||
}
|
||||
|
||||
var twofactorView = twofactorOptions.GetTwoFactorView(
|
||||
owinContext,
|
||||
UmbracoContext,
|
||||
loginModel.Username);
|
||||
|
||||
if (twofactorView.IsNullOrWhiteSpace())
|
||||
{
|
||||
throw new HttpResponseException(
|
||||
Request.CreateErrorResponse(
|
||||
HttpStatusCode.BadRequest,
|
||||
typeof(IUmbracoBackOfficeTwoFactorOptions) + ".GetTwoFactorView returned an empty string"));
|
||||
}
|
||||
|
||||
var attemptedUser = Services.UserService.GetByUsername(loginModel.Username);
|
||||
|
||||
// create a with information to display a custom two factor send code view
|
||||
var verifyResponse = Request.CreateResponse(HttpStatusCode.PaymentRequired, new
|
||||
{
|
||||
twoFactorView = twofactorView,
|
||||
userId = attemptedUser.Id
|
||||
});
|
||||
|
||||
UserManager.RaiseLoginRequiresVerificationEvent(attemptedUser.Id);
|
||||
|
||||
return verifyResponse;
|
||||
}
|
||||
|
||||
// return BadRequest (400), we don't want to return a 401 because that get's intercepted
|
||||
// by our angular helper because it thinks that we need to re-perform the request once we are
|
||||
// authorized and we don't want to return a 403 because angular will show a warning message indicating
|
||||
// that the user doesn't have access to perform this function, we just want to return a normal invalid message.
|
||||
throw new HttpResponseException(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -315,13 +314,13 @@ namespace Umbraco.Web.Editors
|
||||
{
|
||||
throw new HttpResponseException(HttpStatusCode.BadRequest);
|
||||
}
|
||||
var identityUser = await SignInManager.UserManager.FindByEmailAsync(model.Email);
|
||||
var identityUser = await UserManager.FindByEmailAsync(model.Email);
|
||||
if (identityUser != null)
|
||||
{
|
||||
var user = Services.UserService.GetByEmail(model.Email);
|
||||
if (user != null)
|
||||
{
|
||||
var code = await UserManager.GeneratePasswordResetTokenAsync(identityUser.Id);
|
||||
var code = await UserManager.GeneratePasswordResetTokenAsync(identityUser);
|
||||
var callbackUrl = ConstructCallbackUrl(identityUser.Id, code);
|
||||
|
||||
var message = Services.TextService.Localize("resetPasswordEmailCopyFormat",
|
||||
@@ -329,11 +328,12 @@ namespace Umbraco.Web.Editors
|
||||
UmbracoUserExtensions.GetUserCulture(identityUser.Culture, Services.TextService, GlobalSettings),
|
||||
new[] { identityUser.UserName, callbackUrl });
|
||||
|
||||
await UserManager.SendEmailAsync(identityUser.Id,
|
||||
// TODO: Port email service to ASP.NET Core
|
||||
/*await UserManager.SendEmailAsync(identityUser.Id,
|
||||
Services.TextService.Localize("login/resetPasswordEmailCopySubject",
|
||||
// Ensure the culture of the found user is used for the email!
|
||||
UmbracoUserExtensions.GetUserCulture(identityUser.Culture, Services.TextService, GlobalSettings)),
|
||||
message);
|
||||
message);*/
|
||||
|
||||
UserManager.RaiseForgotPasswordRequestedEvent(user.Id);
|
||||
}
|
||||
@@ -350,12 +350,15 @@ namespace Umbraco.Web.Editors
|
||||
public async Task<IEnumerable<string>> Get2FAProviders()
|
||||
{
|
||||
var userId = await SignInManager.GetVerifiedUserIdAsync();
|
||||
if (userId == int.MinValue)
|
||||
if (string.IsNullOrWhiteSpace(userId))
|
||||
{
|
||||
Logger.Warn<AuthenticationController>("Get2FAProviders :: No verified user found, returning 404");
|
||||
throw new HttpResponseException(HttpStatusCode.NotFound);
|
||||
}
|
||||
var userFactors = await UserManager.GetValidTwoFactorProvidersAsync(userId);
|
||||
|
||||
var user = await UserManager.FindByIdAsync(userId);
|
||||
var userFactors = await UserManager.GetValidTwoFactorProvidersAsync(user);
|
||||
|
||||
return userFactors;
|
||||
}
|
||||
|
||||
@@ -366,7 +369,7 @@ namespace Umbraco.Web.Editors
|
||||
throw new HttpResponseException(HttpStatusCode.NotFound);
|
||||
|
||||
var userId = await SignInManager.GetVerifiedUserIdAsync();
|
||||
if (userId == int.MinValue)
|
||||
if (string.IsNullOrWhiteSpace(userId))
|
||||
{
|
||||
Logger.Warn<AuthenticationController>("Get2FAProviders :: No verified user found, returning 404");
|
||||
throw new HttpResponseException(HttpStatusCode.NotFound);
|
||||
@@ -399,18 +402,19 @@ namespace Umbraco.Web.Editors
|
||||
var owinContext = TryGetOwinContext().Result;
|
||||
|
||||
var user = Services.UserService.GetByUsername(userName);
|
||||
switch (result)
|
||||
if (result.Succeeded)
|
||||
{
|
||||
case SignInStatus.Success:
|
||||
UserManager.RaiseLoginSuccessEvent(user.Id);
|
||||
return SetPrincipalAndReturnUserDetail(user, owinContext.Request.User);
|
||||
case SignInStatus.LockedOut:
|
||||
UserManager.RaiseAccountLockedEvent(user.Id);
|
||||
return Request.CreateValidationErrorResponse("User is locked out");
|
||||
case SignInStatus.Failure:
|
||||
default:
|
||||
return Request.CreateValidationErrorResponse("Invalid code");
|
||||
UserManager.RaiseLoginSuccessEvent(user.Id);
|
||||
return SetPrincipalAndReturnUserDetail(user, owinContext.Request.User);
|
||||
}
|
||||
|
||||
if (result.IsLockedOut)
|
||||
{
|
||||
UserManager.RaiseAccountLockedEvent(user.Id);
|
||||
return Request.CreateValidationErrorResponse("User is locked out");
|
||||
}
|
||||
|
||||
return Request.CreateValidationErrorResponse("Invalid code");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -420,25 +424,27 @@ namespace Umbraco.Web.Editors
|
||||
[SetAngularAntiForgeryTokens]
|
||||
public async Task<HttpResponseMessage> PostSetPassword(SetPasswordModel model)
|
||||
{
|
||||
var result = await UserManager.ResetPasswordAsync(model.UserId, model.ResetCode, model.Password);
|
||||
var identityUser = await UserManager.FindByIdAsync(model.UserId.ToString());
|
||||
|
||||
var result = await UserManager.ResetPasswordAsync(identityUser, model.ResetCode, model.Password);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
var lockedOut = await UserManager.IsLockedOutAsync(model.UserId);
|
||||
var lockedOut = await UserManager.IsLockedOutAsync(identityUser);
|
||||
if (lockedOut)
|
||||
{
|
||||
Logger.Info<AuthenticationController>("User {UserId} is currently locked out, unlocking and resetting AccessFailedCount", model.UserId);
|
||||
|
||||
//// var user = await UserManager.FindByIdAsync(model.UserId);
|
||||
var unlockResult = await UserManager.SetLockoutEndDateAsync(model.UserId, DateTimeOffset.Now);
|
||||
var unlockResult = await UserManager.SetLockoutEndDateAsync(identityUser, DateTimeOffset.Now);
|
||||
if (unlockResult.Succeeded == false)
|
||||
{
|
||||
Logger.Warn<AuthenticationController>("Could not unlock for user {UserId} - error {UnlockError}", model.UserId, unlockResult.Errors.First());
|
||||
Logger.Warn<AuthenticationController>("Could not unlock for user {UserId} - error {UnlockError}", model.UserId, unlockResult.Errors.First().Description);
|
||||
}
|
||||
|
||||
var resetAccessFailedCountResult = await UserManager.ResetAccessFailedCountAsync(model.UserId);
|
||||
var resetAccessFailedCountResult = await UserManager.ResetAccessFailedCountAsync(identityUser);
|
||||
if (resetAccessFailedCountResult.Succeeded == false)
|
||||
{
|
||||
Logger.Warn<AuthenticationController>("Could not reset access failed count {UserId} - error {UnlockError}", model.UserId, unlockResult.Errors.First());
|
||||
Logger.Warn<AuthenticationController>("Could not reset access failed count {UserId} - error {UnlockError}", model.UserId, unlockResult.Errors.First().Description);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,13 +452,11 @@ namespace Umbraco.Web.Editors
|
||||
// if user was only invited, then they have not been approved
|
||||
// but a successful forgot password flow (e.g. if their token had expired and they did a forgot password instead of request new invite)
|
||||
// means we have verified their email
|
||||
if (!UserManager.IsEmailConfirmed(model.UserId))
|
||||
if (!await UserManager.IsEmailConfirmedAsync(identityUser))
|
||||
{
|
||||
await UserManager.ConfirmEmailAsync(model.UserId, model.ResetCode);
|
||||
await UserManager.ConfirmEmailAsync(identityUser, model.ResetCode);
|
||||
}
|
||||
|
||||
// if the user is invited, enable their account on forgot password
|
||||
var identityUser = await UserManager.FindByIdAsync(model.UserId);
|
||||
// invited is not approved, never logged in, invited date present
|
||||
/*
|
||||
if (LastLoginDate == default && IsApproved == false && InvitedDate != null)
|
||||
@@ -474,7 +478,7 @@ namespace Umbraco.Web.Editors
|
||||
return Request.CreateResponse(HttpStatusCode.OK);
|
||||
}
|
||||
return Request.CreateValidationErrorResponse(
|
||||
result.Errors.Any() ? result.Errors.First() : "Set password failed");
|
||||
result.Errors.Any() ? result.Errors.First().Description : "Set password failed");
|
||||
}
|
||||
|
||||
|
||||
@@ -561,9 +565,8 @@ namespace Umbraco.Web.Editors
|
||||
{
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError(prefix, error);
|
||||
ModelState.AddModelError(prefix, error.Description);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,7 @@ using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using System.Web.Mvc;
|
||||
using System.Web.UI;
|
||||
using Microsoft.AspNet.Identity;
|
||||
using Microsoft.AspNet.Identity.Owin;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Owin.Security;
|
||||
using Newtonsoft.Json;
|
||||
using Umbraco.Core;
|
||||
@@ -148,25 +147,19 @@ namespace Umbraco.Web.Editors
|
||||
}
|
||||
|
||||
var id = parts[0];
|
||||
int intId;
|
||||
if (int.TryParse(id, out intId) == false)
|
||||
{
|
||||
Logger.Warn<BackOfficeController>("VerifyUser endpoint reached with invalid token: {Invite}", invite);
|
||||
return RedirectToAction("Default");
|
||||
}
|
||||
|
||||
var identityUser = await UserManager.FindByIdAsync(intId);
|
||||
var identityUser = await UserManager.FindByIdAsync(id);
|
||||
if (identityUser == null)
|
||||
{
|
||||
Logger.Warn<BackOfficeController>("VerifyUser endpoint reached with non existing user: {UserId}", id);
|
||||
return RedirectToAction("Default");
|
||||
}
|
||||
|
||||
var result = await UserManager.ConfirmEmailAsync(intId, decoded);
|
||||
var result = await UserManager.ConfirmEmailAsync(identityUser, decoded);
|
||||
|
||||
if (result.Succeeded == false)
|
||||
{
|
||||
Logger.Warn<BackOfficeController>("Could not verify email, Error: {Errors}, Token: {Invite}", string.Join(",", result.Errors), invite);
|
||||
Logger.Warn<BackOfficeController>("Could not verify email, Error: {Errors}, Token: {Invite}", result.Errors.ToErrorMessage(), invite);
|
||||
return new RedirectResult(Url.Action("Default") + "#/login/false?invite=3");
|
||||
}
|
||||
|
||||
@@ -304,10 +297,10 @@ namespace Umbraco.Web.Editors
|
||||
[HttpGet]
|
||||
public async Task<ActionResult> ValidatePasswordResetCode([Bind(Prefix = "u")]int userId, [Bind(Prefix = "r")]string resetCode)
|
||||
{
|
||||
var user = UserManager.FindById(userId);
|
||||
var user = await UserManager.FindByIdAsync(userId.ToString());
|
||||
if (user != null)
|
||||
{
|
||||
var result = await UserManager.UserTokenProvider.ValidateAsync("ResetPassword", resetCode, UserManager, user);
|
||||
var result = await UserManager.VerifyUserTokenAsync(user, "ResetPassword", "ResetPassword", resetCode);
|
||||
if (result)
|
||||
{
|
||||
//Add a flag and redirect for it to be displayed
|
||||
@@ -335,7 +328,11 @@ namespace Umbraco.Web.Editors
|
||||
return RedirectToLocal(Url.Action("Default", "BackOffice"));
|
||||
}
|
||||
|
||||
var result = await UserManager.AddLoginAsync(User.Identity.GetUserId<int>(), loginInfo.Login);
|
||||
var user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
|
||||
if (user == null) throw new InvalidOperationException("Could not find user");
|
||||
|
||||
var result = await UserManager.AddLoginAsync(user,
|
||||
new UserLoginInfo(loginInfo.Login.LoginProvider, loginInfo.Login.ProviderKey, loginInfo.Login.LoginProvider));
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return RedirectToLocal(Url.Action("Default", "BackOffice"));
|
||||
@@ -399,7 +396,7 @@ namespace Umbraco.Web.Editors
|
||||
}
|
||||
|
||||
// Sign in the user with this external login provider if the user already has a login
|
||||
var user = await UserManager.FindAsync(loginInfo.Login);
|
||||
var user = await UserManager.FindByLoginAsync(loginInfo.Login.LoginProvider, loginInfo.Login.ProviderKey);
|
||||
if (user != null)
|
||||
{
|
||||
// TODO: It might be worth keeping some of the claims associated with the ExternalLoginInfo, in which case we
|
||||
@@ -490,21 +487,22 @@ namespace Umbraco.Web.Editors
|
||||
|
||||
if (userCreationResult.Succeeded == false)
|
||||
{
|
||||
ViewData.SetExternalSignInError(userCreationResult.Errors);
|
||||
ViewData.SetExternalSignInError(userCreationResult.Errors.Select(x => x.Description).ToList());
|
||||
}
|
||||
else
|
||||
{
|
||||
var linkResult = await UserManager.AddLoginAsync(autoLinkUser.Id, loginInfo.Login);
|
||||
var linkResult = await UserManager.AddLoginAsync(autoLinkUser,
|
||||
new UserLoginInfo(loginInfo.Login.LoginProvider, loginInfo.Login.ProviderKey, loginInfo.Login.LoginProvider));
|
||||
if (linkResult.Succeeded == false)
|
||||
{
|
||||
ViewData.SetExternalSignInError(linkResult.Errors);
|
||||
ViewData.SetExternalSignInError(linkResult.Errors.Select(x => x.Description).ToList());
|
||||
|
||||
//If this fails, we should really delete the user since it will be in an inconsistent state!
|
||||
var deleteResult = await UserManager.DeleteAsync(autoLinkUser);
|
||||
if (deleteResult.Succeeded == false)
|
||||
{
|
||||
//DOH! ... this isn't good, combine all errors to be shown
|
||||
ViewData.SetExternalSignInError(linkResult.Errors.Concat(deleteResult.Errors));
|
||||
ViewData.SetExternalSignInError(linkResult.Errors.Concat(deleteResult.Errors).Select(x => x.Description).ToList());
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -21,6 +21,7 @@ using Umbraco.Core.Hosting;
|
||||
using Umbraco.Core.IO;
|
||||
using Umbraco.Core.Runtime;
|
||||
using Umbraco.Core.WebAssets;
|
||||
using Umbraco.Web.Security;
|
||||
|
||||
namespace Umbraco.Web.Editors
|
||||
{
|
||||
|
||||
@@ -158,15 +158,16 @@ namespace Umbraco.Web.Editors
|
||||
[OverrideAuthorization]
|
||||
public async Task<UserDetail> PostSetInvitedUserPassword([FromBody]string newPassword)
|
||||
{
|
||||
var result = await UserManager.AddPasswordAsync(Security.GetUserId().ResultOr(0), newPassword);
|
||||
var user = await UserManager.FindByIdAsync(Security.GetUserId().ResultOr(0).ToString());
|
||||
if (user == null) throw new InvalidOperationException("Could not find user");
|
||||
|
||||
var result = await UserManager.AddPasswordAsync(user, newPassword);
|
||||
|
||||
if (result.Succeeded == false)
|
||||
{
|
||||
//it wasn't successful, so add the change error to the model state, we've name the property alias _umb_password on the form
|
||||
// so that is why it is being used here.
|
||||
ModelState.AddModelError(
|
||||
"value",
|
||||
string.Join(", ", result.Errors));
|
||||
ModelState.AddModelError("value", result.Errors.ToErrorMessage());
|
||||
|
||||
throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState));
|
||||
}
|
||||
|
||||
@@ -44,6 +44,13 @@ namespace Umbraco.Web.Editors
|
||||
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Cannot set an empty password", new[] { "value" }) });
|
||||
}
|
||||
|
||||
var backOfficeIdentityUser = await userMgr.FindByIdAsync(savingUser.Id.ToString());
|
||||
if (backOfficeIdentityUser == null)
|
||||
{
|
||||
//this really shouldn't ever happen... but just in case
|
||||
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password could not be verified", new[] { "oldPassword" }) });
|
||||
}
|
||||
|
||||
//Are we just changing another user's password?
|
||||
if (passwordModel.OldPassword.IsNullOrWhiteSpace())
|
||||
{
|
||||
@@ -60,13 +67,13 @@ namespace Umbraco.Web.Editors
|
||||
}
|
||||
|
||||
//ok, we should be able to reset it
|
||||
var resetToken = await userMgr.GeneratePasswordResetTokenAsync(savingUser.Id);
|
||||
var resetToken = await userMgr.GeneratePasswordResetTokenAsync(backOfficeIdentityUser);
|
||||
|
||||
var resetResult = await userMgr.ChangePasswordWithResetAsync(savingUser.Id, resetToken, passwordModel.NewPassword);
|
||||
|
||||
if (resetResult.Succeeded == false)
|
||||
{
|
||||
var errors = string.Join(". ", resetResult.Errors);
|
||||
var errors = resetResult.Errors.ToErrorMessage();
|
||||
_logger.Warn<PasswordChanger>("Could not reset user password {PasswordErrors}", errors);
|
||||
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult(errors, new[] { "value" }) });
|
||||
}
|
||||
@@ -74,15 +81,6 @@ namespace Umbraco.Web.Editors
|
||||
return Attempt.Succeed(new PasswordChangedModel());
|
||||
}
|
||||
|
||||
//we're changing our own password...
|
||||
|
||||
//get the user
|
||||
var backOfficeIdentityUser = await userMgr.FindByIdAsync(savingUser.Id);
|
||||
if (backOfficeIdentityUser == null)
|
||||
{
|
||||
//this really shouldn't ever happen... but just in case
|
||||
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password could not be verified", new[] { "oldPassword" }) });
|
||||
}
|
||||
//is the old password correct?
|
||||
var validateResult = await userMgr.CheckPasswordAsync(backOfficeIdentityUser, passwordModel.OldPassword);
|
||||
if (validateResult == false)
|
||||
@@ -91,11 +89,11 @@ namespace Umbraco.Web.Editors
|
||||
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Incorrect password", new[] { "oldPassword" }) });
|
||||
}
|
||||
//can we change to the new password?
|
||||
var changeResult = await userMgr.ChangePasswordAsync(savingUser.Id, passwordModel.OldPassword, passwordModel.NewPassword);
|
||||
var changeResult = await userMgr.ChangePasswordAsync(backOfficeIdentityUser, passwordModel.OldPassword, passwordModel.NewPassword);
|
||||
if (changeResult.Succeeded == false)
|
||||
{
|
||||
//no, fail with error messages for "password"
|
||||
var errors = string.Join(". ", changeResult.Errors);
|
||||
var errors = changeResult.Errors.ToErrorMessage();
|
||||
_logger.Warn<PasswordChanger>("Could not change user password {PasswordErrors}", errors);
|
||||
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult(errors, new[] { "password" }) });
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using System.Web.Http;
|
||||
using System.Web.Mvc;
|
||||
using Microsoft.AspNet.Identity;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Cache;
|
||||
using Umbraco.Core.Configuration;
|
||||
@@ -20,7 +19,6 @@ using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.Editors;
|
||||
using Umbraco.Core.Models.Membership;
|
||||
using Umbraco.Core.Persistence;
|
||||
using Umbraco.Core.Security;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Core.Strings;
|
||||
using Umbraco.Web.Editors.Filters;
|
||||
@@ -38,6 +36,7 @@ using Umbraco.Core.Configuration.UmbracoSettings;
|
||||
using Umbraco.Core.Hosting;
|
||||
using Umbraco.Web.Routing;
|
||||
using Umbraco.Core.Media;
|
||||
using Umbraco.Web.Security;
|
||||
|
||||
namespace Umbraco.Web.Editors
|
||||
{
|
||||
@@ -322,26 +321,21 @@ namespace Umbraco.Web.Editors
|
||||
if (created.Succeeded == false)
|
||||
{
|
||||
throw new HttpResponseException(
|
||||
Request.CreateNotificationValidationErrorResponse(string.Join(", ", created.Errors)));
|
||||
Request.CreateNotificationValidationErrorResponse(created.Errors.ToErrorMessage()));
|
||||
}
|
||||
|
||||
//we need to generate a password, however we can only do that if the user manager has a password validator that
|
||||
//we can read values from
|
||||
var passwordValidator = UserManager.PasswordValidator as PasswordValidator;
|
||||
var resetPassword = string.Empty;
|
||||
if (passwordValidator != null)
|
||||
string resetPassword;
|
||||
var password = UserManager.GeneratePassword();
|
||||
|
||||
var result = await UserManager.AddPasswordAsync(identityUser, password);
|
||||
if (result.Succeeded == false)
|
||||
{
|
||||
var password = UserManager.GeneratePassword();
|
||||
|
||||
var result = await UserManager.AddPasswordAsync(identityUser.Id, password);
|
||||
if (result.Succeeded == false)
|
||||
{
|
||||
throw new HttpResponseException(
|
||||
Request.CreateNotificationValidationErrorResponse(string.Join(", ", created.Errors)));
|
||||
}
|
||||
resetPassword = password;
|
||||
throw new HttpResponseException(
|
||||
Request.CreateNotificationValidationErrorResponse(created.Errors.ToErrorMessage()));
|
||||
}
|
||||
|
||||
resetPassword = password;
|
||||
|
||||
//now re-look the user back up which will now exist
|
||||
var user = Services.UserService.GetByEmail(userSave.Email);
|
||||
|
||||
@@ -416,7 +410,7 @@ namespace Umbraco.Web.Editors
|
||||
if (created.Succeeded == false)
|
||||
{
|
||||
throw new HttpResponseException(
|
||||
Request.CreateNotificationValidationErrorResponse(string.Join(", ", created.Errors)));
|
||||
Request.CreateNotificationValidationErrorResponse(created.Errors.ToErrorMessage()));
|
||||
}
|
||||
|
||||
//now re-look the user back up
|
||||
@@ -476,7 +470,8 @@ namespace Umbraco.Web.Editors
|
||||
|
||||
private async Task SendUserInviteEmailAsync(UserBasic userDisplay, string from, string fromEmail, IUser to, string message)
|
||||
{
|
||||
var token = await UserManager.GenerateEmailConfirmationTokenAsync((int)userDisplay.Id);
|
||||
var user = await UserManager.FindByIdAsync(((int) userDisplay.Id).ToString());
|
||||
var token = await UserManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
|
||||
var inviteToken = string.Format("{0}{1}{2}",
|
||||
(int)userDisplay.Id,
|
||||
@@ -505,7 +500,8 @@ namespace Umbraco.Web.Editors
|
||||
UmbracoUserExtensions.GetUserCulture(to.Language, Services.TextService, GlobalSettings),
|
||||
new[] { userDisplay.Name, from, message, inviteUri.ToString(), fromEmail });
|
||||
|
||||
await UserManager.EmailService.SendAsync(
|
||||
// TODO: Port email service to ASP.NET Core
|
||||
/*await UserManager.EmailService.SendAsync(
|
||||
//send the special UmbracoEmailMessage which configures it's own sender
|
||||
//to allow for events to handle sending the message if no smtp is configured
|
||||
new UmbracoEmailMessage(new EmailSender(GlobalSettings, true))
|
||||
@@ -513,7 +509,7 @@ namespace Umbraco.Web.Editors
|
||||
Body = emailBody,
|
||||
Destination = userDisplay.Email,
|
||||
Subject = emailSubject
|
||||
});
|
||||
});*/
|
||||
|
||||
}
|
||||
|
||||
@@ -706,34 +702,29 @@ namespace Umbraco.Web.Editors
|
||||
[AdminUsersAuthorize("userIds")]
|
||||
public async Task<HttpResponseMessage> PostUnlockUsers([FromUri]int[] userIds)
|
||||
{
|
||||
if (userIds.Length <= 0)
|
||||
return Request.CreateResponse(HttpStatusCode.OK);
|
||||
|
||||
if (userIds.Length == 1)
|
||||
{
|
||||
var unlockResult = await UserManager.SetLockoutEndDateAsync(userIds[0], DateTimeOffset.Now);
|
||||
if (unlockResult.Succeeded == false)
|
||||
{
|
||||
return Request.CreateValidationErrorResponse(
|
||||
string.Format("Could not unlock for user {0} - error {1}", userIds[0], unlockResult.Errors.First()));
|
||||
}
|
||||
var user = await UserManager.FindByIdAsync(userIds[0]);
|
||||
return Request.CreateNotificationSuccessResponse(
|
||||
Services.TextService.Localize("speechBubbles/unlockUserSuccess", new[] { user.Name }));
|
||||
}
|
||||
if (userIds.Length <= 0) return Request.CreateResponse(HttpStatusCode.OK);
|
||||
|
||||
foreach (var u in userIds)
|
||||
{
|
||||
var unlockResult = await UserManager.SetLockoutEndDateAsync(u, DateTimeOffset.Now);
|
||||
var user = await UserManager.FindByIdAsync(u.ToString());
|
||||
if (user == null) throw new InvalidOperationException();
|
||||
|
||||
var unlockResult = await UserManager.SetLockoutEndDateAsync(user, DateTimeOffset.Now);
|
||||
if (unlockResult.Succeeded == false)
|
||||
{
|
||||
return Request.CreateValidationErrorResponse(
|
||||
string.Format("Could not unlock for user {0} - error {1}", u, unlockResult.Errors.First()));
|
||||
string.Format("Could not unlock for user {0} - error {1}", u, unlockResult.Errors.ToErrorMessage()));
|
||||
}
|
||||
|
||||
if (userIds.Length == 1)
|
||||
{
|
||||
return Request.CreateNotificationSuccessResponse(
|
||||
Services.TextService.Localize("speechBubbles/unlockUserSuccess", new[] {user.Name}));
|
||||
}
|
||||
}
|
||||
|
||||
return Request.CreateNotificationSuccessResponse(
|
||||
Services.TextService.Localize("speechBubbles/unlockUsersSuccess", new[] { userIds.Length.ToString() }));
|
||||
Services.TextService.Localize("speechBubbles/unlockUsersSuccess", new[] {userIds.Length.ToString()}));
|
||||
}
|
||||
|
||||
[AdminUsersAuthorize("userIds")]
|
||||
|
||||
@@ -15,6 +15,7 @@ using Umbraco.Web.Composing;
|
||||
using Umbraco.Web.Editors;
|
||||
using Umbraco.Web.Features;
|
||||
using Umbraco.Web.Models;
|
||||
using Umbraco.Web.Security;
|
||||
using Umbraco.Web.Trees;
|
||||
using Umbraco.Web.WebAssets;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
@@ -11,6 +12,7 @@ using Umbraco.Core.Migrations.Install;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Web.Install.Models;
|
||||
using Umbraco.Core.Configuration.UmbracoSettings;
|
||||
using Umbraco.Web.Security;
|
||||
|
||||
namespace Umbraco.Web.Install.InstallSteps
|
||||
{
|
||||
@@ -54,9 +56,14 @@ namespace Umbraco.Web.Install.InstallSteps
|
||||
{
|
||||
throw new InvalidOperationException("Could not find the super user!");
|
||||
}
|
||||
admin.Email = user.Email.Trim();
|
||||
admin.Name = user.Name.Trim();
|
||||
admin.Username = user.Email.Trim();
|
||||
|
||||
_userService.Save(admin);
|
||||
|
||||
var userManager = _httpContextAccessor.GetRequiredHttpContext().GetOwinContext().GetBackOfficeUserManager();
|
||||
var membershipUser = await userManager.FindByIdAsync(Constants.Security.SuperUserId);
|
||||
var membershipUser = await userManager.FindByIdAsync(Constants.Security.SuperUserId.ToString());
|
||||
if (membershipUser == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
@@ -64,19 +71,15 @@ namespace Umbraco.Web.Install.InstallSteps
|
||||
}
|
||||
|
||||
//To change the password here we actually need to reset it since we don't have an old one to use to change
|
||||
var resetToken = await userManager.GeneratePasswordResetTokenAsync(membershipUser.Id);
|
||||
var resetResult =
|
||||
await userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, user.Password.Trim());
|
||||
var resetToken = await userManager.GeneratePasswordResetTokenAsync(membershipUser);
|
||||
if (string.IsNullOrWhiteSpace(resetToken))
|
||||
throw new InvalidOperationException("Could not reset password: unable to generate internal reset token");
|
||||
|
||||
var resetResult = await userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, user.Password.Trim());
|
||||
if (!resetResult.Succeeded)
|
||||
{
|
||||
throw new InvalidOperationException("Could not reset password: " + string.Join(", ", resetResult.Errors));
|
||||
}
|
||||
throw new InvalidOperationException("Could not reset password: " + string.Join(", ", resetResult.Errors.ToErrorMessage()));
|
||||
|
||||
admin.Email = user.Email.Trim();
|
||||
admin.Name = user.Name.Trim();
|
||||
admin.Username = user.Email.Trim();
|
||||
|
||||
_userService.Save(admin);
|
||||
|
||||
if (user.SubscribeToNewsLetter)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNet.Identity;
|
||||
using Umbraco.Core.Models.Identity;
|
||||
|
||||
namespace Umbraco.Web.Models.Identity
|
||||
@@ -13,7 +12,7 @@ namespace Umbraco.Web.Models.Identity
|
||||
/// This class normally exists inside of the EntityFramework library, not sure why MS chose to explicitly put it there but we don't want
|
||||
/// references to that so we will create our own here
|
||||
/// </remarks>
|
||||
public class IdentityUser<TKey, TLogin, TRole, TClaim> : IUser<TKey>
|
||||
public class IdentityUser<TKey, TLogin, TRole, TClaim>
|
||||
where TLogin : IIdentityUserLogin
|
||||
//NOTE: Making our role id a string
|
||||
where TRole : IdentityUserRole<string>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNet.Identity;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Umbraco.Core.Models.Identity;
|
||||
|
||||
namespace Umbraco.Web.Models.Identity
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
using System;
|
||||
using System.Web;
|
||||
using Microsoft.AspNet.Identity.Owin;
|
||||
using Microsoft.Owin;
|
||||
using Microsoft.Owin.Security;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Models.Identity;
|
||||
using Umbraco.Core.Security;
|
||||
using Umbraco.Web.Models.Identity;
|
||||
using Umbraco.Web.Security;
|
||||
|
||||
@@ -54,7 +51,7 @@ namespace Umbraco.Web
|
||||
var ctx = owinContext.Get<HttpContextBase>(typeof(HttpContextBase).FullName);
|
||||
return ctx == null ? Attempt<HttpContextBase>.Fail() : Attempt.Succeed(ctx);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the back office sign in manager out of OWIN
|
||||
/// </summary>
|
||||
@@ -83,6 +80,25 @@ namespace Umbraco.Web
|
||||
return marker.GetManager(owinContext)
|
||||
?? throw new NullReferenceException($"Could not resolve an instance of {typeof (BackOfficeUserManager<BackOfficeIdentityUser>)} from the {typeof (IOwinContext)}.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adapted from Microsoft.AspNet.Identity.Owin.OwinContextExtensions
|
||||
/// </summary>
|
||||
public static T Get<T>(this IOwinContext context)
|
||||
{
|
||||
if (context == null) throw new ArgumentNullException(nameof(context));
|
||||
return context.Get<T>(GetKey(typeof(T)));
|
||||
}
|
||||
|
||||
public static IOwinContext Set<T>(this IOwinContext context, T value)
|
||||
{
|
||||
if (context == null) throw new ArgumentNullException(nameof(context));
|
||||
return context.Set(GetKey(typeof(T)), value);
|
||||
}
|
||||
|
||||
private static string GetKey(Type t)
|
||||
{
|
||||
return "AspNet.Identity.Owin:" + t.AssemblyQualifiedName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Microsoft.AspNet.Identity;
|
||||
using Microsoft.AspNet.Identity.Owin;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Owin;
|
||||
using Microsoft.Owin.Extensions;
|
||||
using Microsoft.Owin.Logging;
|
||||
@@ -15,11 +15,8 @@ using Umbraco.Core.Cache;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Configuration.UmbracoSettings;
|
||||
using Umbraco.Core.Hosting;
|
||||
using Umbraco.Core.IO;
|
||||
using Umbraco.Core.Mapping;
|
||||
using Umbraco.Core.Models.Identity;
|
||||
using Umbraco.Net;
|
||||
using Umbraco.Core.Security;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Web.Composing;
|
||||
using Umbraco.Web.Models.Identity;
|
||||
@@ -35,18 +32,10 @@ namespace Umbraco.Web.Security
|
||||
/// <summary>
|
||||
/// Configure Default Identity User Manager for Umbraco
|
||||
/// </summary>
|
||||
/// <param name="app"></param>
|
||||
/// <param name="services"></param>
|
||||
/// <param name="mapper"></param>
|
||||
/// <param name="contentSettings"></param>
|
||||
/// <param name="globalSettings"></param>
|
||||
/// <param name="passwordConfiguration"></param>
|
||||
/// <param name="ipResolver"></param>
|
||||
public static void ConfigureUserManagerForUmbracoBackOffice(this IAppBuilder app,
|
||||
ServiceContext services,
|
||||
UmbracoMapper mapper,
|
||||
IContentSettings contentSettings,
|
||||
IGlobalSettings globalSettings,
|
||||
UmbracoMapper mapper,
|
||||
// TODO: This could probably be optional?
|
||||
IPasswordConfiguration passwordConfiguration,
|
||||
IIpResolver ipResolver)
|
||||
@@ -56,35 +45,28 @@ namespace Umbraco.Web.Security
|
||||
//Configure Umbraco user manager to be created per request
|
||||
app.CreatePerOwinContext<BackOfficeUserManager>(
|
||||
(options, owinContext) => BackOfficeUserManager.Create(
|
||||
options,
|
||||
services.UserService,
|
||||
services.EntityService,
|
||||
services.ExternalLoginService,
|
||||
mapper,
|
||||
contentSettings,
|
||||
globalSettings,
|
||||
mapper,
|
||||
passwordConfiguration,
|
||||
ipResolver));
|
||||
ipResolver,
|
||||
new IdentityErrorDescriber(),
|
||||
app.GetDataProtectionProvider(),
|
||||
new NullLogger<BackOfficeUserManager<BackOfficeIdentityUser>>()));
|
||||
|
||||
app.SetBackOfficeUserManagerType<BackOfficeUserManager, BackOfficeIdentityUser>();
|
||||
|
||||
//Create a sign in manager per request
|
||||
app.CreatePerOwinContext<BackOfficeSignInManager>((options, context) => BackOfficeSignInManager.Create(options, context, globalSettings, app.CreateLogger<BackOfficeSignInManager>()));
|
||||
app.CreatePerOwinContext<BackOfficeSignInManager>((options, context) => BackOfficeSignInManager.Create(context, globalSettings, app.CreateLogger<BackOfficeSignInManager>()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure a custom UserStore with the Identity User Manager for Umbraco
|
||||
/// </summary>
|
||||
/// <param name="app"></param>
|
||||
/// <param name="runtimeState"></param>
|
||||
/// <param name="globalSettings"></param>
|
||||
/// <param name="customUserStore"></param>
|
||||
/// <param name="contentSettings"></param>
|
||||
/// <param name="passwordConfiguration"></param>
|
||||
/// <param name="ipResolver"></param>
|
||||
public static void ConfigureUserManagerForUmbracoBackOffice(this IAppBuilder app,
|
||||
IRuntimeState runtimeState,
|
||||
IContentSettings contentSettings,
|
||||
IGlobalSettings globalSettings,
|
||||
BackOfficeUserStore customUserStore,
|
||||
// TODO: This could probably be optional?
|
||||
@@ -97,44 +79,17 @@ namespace Umbraco.Web.Security
|
||||
//Configure Umbraco user manager to be created per request
|
||||
app.CreatePerOwinContext<BackOfficeUserManager>(
|
||||
(options, owinContext) => BackOfficeUserManager.Create(
|
||||
options,
|
||||
customUserStore,
|
||||
contentSettings,
|
||||
passwordConfiguration,
|
||||
ipResolver,
|
||||
globalSettings));
|
||||
customUserStore,
|
||||
new IdentityErrorDescriber(),
|
||||
app.GetDataProtectionProvider(),
|
||||
new NullLogger<BackOfficeUserManager<BackOfficeIdentityUser>>()));
|
||||
|
||||
app.SetBackOfficeUserManagerType<BackOfficeUserManager, BackOfficeIdentityUser>();
|
||||
|
||||
//Create a sign in manager per request
|
||||
app.CreatePerOwinContext<BackOfficeSignInManager>((options, context) => BackOfficeSignInManager.Create(options, context, globalSettings, app.CreateLogger(typeof(BackOfficeSignInManager).FullName)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure a custom BackOfficeUserManager for Umbraco
|
||||
/// </summary>
|
||||
/// <param name="app"></param>
|
||||
/// <param name="runtimeState"></param>
|
||||
/// <param name="globalSettings"></param>
|
||||
/// <param name="userManager"></param>
|
||||
public static void ConfigureUserManagerForUmbracoBackOffice<TManager, TUser>(this IAppBuilder app,
|
||||
IRuntimeState runtimeState,
|
||||
IGlobalSettings globalSettings,
|
||||
Func<IdentityFactoryOptions<TManager>, IOwinContext, TManager> userManager)
|
||||
where TManager : BackOfficeUserManager<TUser>
|
||||
where TUser : BackOfficeIdentityUser
|
||||
{
|
||||
if (runtimeState == null) throw new ArgumentNullException(nameof(runtimeState));
|
||||
if (userManager == null) throw new ArgumentNullException(nameof(userManager));
|
||||
|
||||
//Configure Umbraco user manager to be created per request
|
||||
app.CreatePerOwinContext<TManager>(userManager);
|
||||
|
||||
app.SetBackOfficeUserManagerType<TManager, TUser>();
|
||||
|
||||
//Create a sign in manager per request
|
||||
app.CreatePerOwinContext<BackOfficeSignInManager>(
|
||||
(options, context) => BackOfficeSignInManager.Create(options, context, globalSettings, app.CreateLogger(typeof(BackOfficeSignInManager).FullName)));
|
||||
app.CreatePerOwinContext<BackOfficeSignInManager>((options, context) => BackOfficeSignInManager.Create(context, globalSettings, app.CreateLogger(typeof(BackOfficeSignInManager).FullName)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -197,11 +152,11 @@ namespace Umbraco.Web.Security
|
||||
// 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 = SecurityStampValidator
|
||||
.OnValidateIdentity<BackOfficeUserManager, BackOfficeIdentityUser, int>(
|
||||
OnValidateIdentity = UmbracoSecurityStampValidator
|
||||
.OnValidateIdentity<BackOfficeSignInManager, BackOfficeUserManager, BackOfficeIdentityUser>(
|
||||
TimeSpan.FromMinutes(30),
|
||||
(manager, user) => manager.GenerateUserIdentityAsync(user),
|
||||
identity => identity.GetUserId<int>()),
|
||||
(signInManager, manager, user) => signInManager.CreateUserIdentityAsync(user),
|
||||
identity => identity.GetUserId()),
|
||||
|
||||
};
|
||||
|
||||
@@ -464,5 +419,41 @@ namespace Umbraco.Web.Security
|
||||
|
||||
return authOptions;
|
||||
}
|
||||
public static IAppBuilder CreatePerOwinContext<T>(this IAppBuilder app, Func<T> createCallback)
|
||||
where T : class, IDisposable
|
||||
{
|
||||
return CreatePerOwinContext<T>(app, (options, context) => createCallback());
|
||||
}
|
||||
|
||||
public static IAppBuilder CreatePerOwinContext<T>(this IAppBuilder app,
|
||||
Func<IdentityFactoryOptions<T>, IOwinContext, T> createCallback) where T : class, IDisposable
|
||||
{
|
||||
if (app == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(app));
|
||||
}
|
||||
return app.CreatePerOwinContext(createCallback, (options, instance) => instance.Dispose());
|
||||
}
|
||||
|
||||
public static IAppBuilder CreatePerOwinContext<T>(this IAppBuilder app,
|
||||
Func<IdentityFactoryOptions<T>, IOwinContext, T> createCallback,
|
||||
Action<IdentityFactoryOptions<T>, T> disposeCallback) where T : class, IDisposable
|
||||
{
|
||||
if (app == null) throw new ArgumentNullException(nameof(app));
|
||||
if (createCallback == null) throw new ArgumentNullException(nameof(createCallback));
|
||||
if (disposeCallback == null) throw new ArgumentNullException(nameof(disposeCallback));
|
||||
|
||||
app.Use(typeof(IdentityFactoryMiddleware<T, IdentityFactoryOptions<T>>),
|
||||
new IdentityFactoryOptions<T>
|
||||
{
|
||||
DataProtectionProvider = app.GetDataProtectionProvider(),
|
||||
Provider = new IdentityFactoryProvider<T>
|
||||
{
|
||||
OnCreate = createCallback,
|
||||
OnDispose = disposeCallback
|
||||
}
|
||||
});
|
||||
return app;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ using System.Security.Claims;
|
||||
using System.Security.Principal;
|
||||
using System.Threading;
|
||||
using System.Web;
|
||||
using Microsoft.AspNet.Identity;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Owin;
|
||||
using Microsoft.Owin.Security;
|
||||
using Newtonsoft.Json;
|
||||
@@ -384,5 +384,11 @@ namespace Umbraco.Web.Security
|
||||
/// </summary>
|
||||
private static readonly ConcurrentDictionary<string, CultureInfo> UserCultures = new ConcurrentDictionary<string, CultureInfo>();
|
||||
|
||||
public static string ToErrorMessage(this IEnumerable<IdentityError> errors)
|
||||
{
|
||||
if (errors == null) throw new ArgumentNullException(nameof(errors));
|
||||
return string.Join(", ", errors.Select(x => x.Description).ToList());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Identity;
|
||||
using Microsoft.AspNet.Identity.Owin;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Owin.Security;
|
||||
using Umbraco.Core;
|
||||
|
||||
namespace Umbraco.Web.Security
|
||||
{
|
||||
@@ -26,11 +27,12 @@ namespace Umbraco.Web.Security
|
||||
{
|
||||
name = name.Replace(" ", "");
|
||||
}
|
||||
|
||||
var email = result.Identity.FindFirstValue(ClaimTypes.Email);
|
||||
return new ExternalLoginInfo
|
||||
{
|
||||
ExternalIdentity = result.Identity,
|
||||
Login = new UserLoginInfo(idClaim.Issuer, idClaim.Value),
|
||||
Login = new UserLoginInfo(idClaim.Issuer, idClaim.Value, idClaim.Issuer),
|
||||
DefaultUserName = name,
|
||||
Email = email
|
||||
};
|
||||
@@ -76,11 +78,39 @@ namespace Umbraco.Web.Security
|
||||
/// <returns></returns>
|
||||
public static async Task<ExternalLoginInfo> GetExternalLoginInfoAsync(this IAuthenticationManager manager, string authenticationType)
|
||||
{
|
||||
if (manager == null)
|
||||
{
|
||||
throw new ArgumentNullException("manager");
|
||||
}
|
||||
if (manager == null) throw new ArgumentNullException(nameof(manager));
|
||||
return GetExternalLoginInfo(await manager.AuthenticateAsync(authenticationType));
|
||||
}
|
||||
|
||||
public static IEnumerable<AuthenticationDescription> GetExternalAuthenticationTypes(this IAuthenticationManager manager)
|
||||
{
|
||||
if (manager == null) throw new ArgumentNullException(nameof(manager));
|
||||
return manager.GetAuthenticationTypes(d => d.Properties != null && d.Caption != null);
|
||||
}
|
||||
|
||||
public static ClaimsIdentity CreateTwoFactorRememberBrowserIdentity(this IAuthenticationManager manager, string userId)
|
||||
{
|
||||
if (manager == null) throw new ArgumentNullException(nameof(manager));
|
||||
|
||||
var rememberBrowserIdentity = new ClaimsIdentity(Constants.Web.TwoFactorRememberBrowserCookie);
|
||||
rememberBrowserIdentity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userId));
|
||||
|
||||
return rememberBrowserIdentity;
|
||||
}
|
||||
}
|
||||
|
||||
public class ExternalLoginInfo
|
||||
{
|
||||
/// <summary>Associated login data</summary>
|
||||
public UserLoginInfo Login { get; set; }
|
||||
|
||||
/// <summary>Suggested user name for a user</summary>
|
||||
public string DefaultUserName { get; set; }
|
||||
|
||||
/// <summary>Email claim from the external identity</summary>
|
||||
public string Email { get; set; }
|
||||
|
||||
/// <summary>The external identity</summary>
|
||||
public ClaimsIdentity ExternalIdentity { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Identity;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Models.Identity;
|
||||
using Umbraco.Core.Security;
|
||||
using Umbraco.Web.Models.Identity;
|
||||
using Constants = Umbraco.Core.Constants;
|
||||
|
||||
namespace Umbraco.Web.Security
|
||||
{
|
||||
public class BackOfficeClaimsIdentityFactory<T> : ClaimsIdentityFactory<T, int>
|
||||
where T: BackOfficeIdentityUser
|
||||
{
|
||||
public BackOfficeClaimsIdentityFactory()
|
||||
{
|
||||
SecurityStampClaimType = Constants.Security.SessionIdClaimType;
|
||||
UserNameClaimType = ClaimTypes.Name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a ClaimsIdentity from a user
|
||||
/// </summary>
|
||||
/// <param name="manager"/><param name="user"/><param name="authenticationType"/>
|
||||
/// <returns/>
|
||||
public override async Task<ClaimsIdentity> CreateAsync(UserManager<T, int> manager, T user, string authenticationType)
|
||||
{
|
||||
var baseIdentity = await base.CreateAsync(manager, user, authenticationType);
|
||||
|
||||
var umbracoIdentity = new UmbracoBackOfficeIdentity(baseIdentity,
|
||||
user.Id,
|
||||
user.UserName,
|
||||
user.Name,
|
||||
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 umbracoIdentity;
|
||||
}
|
||||
}
|
||||
|
||||
public class BackOfficeClaimsIdentityFactory : BackOfficeClaimsIdentityFactory<BackOfficeIdentityUser>
|
||||
{ }
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Core.Security;
|
||||
using Umbraco.Web.Models.Identity;
|
||||
|
||||
namespace Umbraco.Web.Security
|
||||
{
|
||||
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<IdentityOptions> optionsAccessor)
|
||||
: base(userManager, optionsAccessor)
|
||||
{
|
||||
}
|
||||
|
||||
public override async Task<ClaimsPrincipal> CreateAsync(TUser user)
|
||||
{
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
|
||||
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,
|
||||
user.UserName,
|
||||
user.Name,
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Identity;
|
||||
using Microsoft.Owin;
|
||||
using Microsoft.Owin.Security.Cookies;
|
||||
using Umbraco.Core;
|
||||
@@ -58,7 +57,7 @@ namespace Umbraco.Web.Security
|
||||
if (context?.OwinContext?.Authentication?.User?.Identity != null)
|
||||
{
|
||||
var claimsIdentity = context.OwinContext.Authentication.User.Identity as ClaimsIdentity;
|
||||
var sessionId = claimsIdentity.FindFirstValue(Core.Constants.Security.SessionIdClaimType);
|
||||
var sessionId = claimsIdentity.FindFirstValue(Constants.Security.SessionIdClaimType);
|
||||
if (sessionId.IsNullOrWhiteSpace() == false && Guid.TryParse(sessionId, out var guidSession))
|
||||
{
|
||||
_userService.ClearLoginSession(guidSession);
|
||||
|
||||
@@ -2,47 +2,63 @@
|
||||
using System.Diagnostics;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Identity;
|
||||
using Microsoft.AspNet.Identity.Owin;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Owin;
|
||||
using Microsoft.Owin.Logging;
|
||||
using Microsoft.Owin.Security;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Models.Identity;
|
||||
using Umbraco.Core.Security;
|
||||
using Umbraco.Web.Models.Identity;
|
||||
using Constants = Umbraco.Core.Constants;
|
||||
|
||||
namespace Umbraco.Web.Security
|
||||
{
|
||||
// TODO: In v8 we need to change this to use an int? nullable TKey instead, see notes against overridden TwoFactorSignInAsync
|
||||
public class BackOfficeSignInManager : SignInManager<BackOfficeIdentityUser, int>
|
||||
/// <summary>
|
||||
/// Custom sign in manager due to SignInManager not being .NET Standard.
|
||||
/// Code ported from Umbraco's BackOfficeSignInManager.
|
||||
/// Can be removed once the web project moves to .NET Core.
|
||||
/// </summary>
|
||||
public class BackOfficeSignInManager : IDisposable
|
||||
{
|
||||
private readonly BackOfficeUserManager<BackOfficeIdentityUser> _userManager;
|
||||
private readonly IUserClaimsPrincipalFactory<BackOfficeIdentityUser> _claimsPrincipalFactory;
|
||||
private readonly IAuthenticationManager _authenticationManager;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IOwinRequest _request;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly IOwinRequest _request;
|
||||
|
||||
public BackOfficeSignInManager(UserManager<BackOfficeIdentityUser, int> userManager, IAuthenticationManager authenticationManager, ILogger logger, IGlobalSettings globalSettings, IOwinRequest request)
|
||||
: base(userManager, authenticationManager)
|
||||
public BackOfficeSignInManager(
|
||||
BackOfficeUserManager<BackOfficeIdentityUser> userManager,
|
||||
IUserClaimsPrincipalFactory<BackOfficeIdentityUser> claimsPrincipalFactory,
|
||||
IAuthenticationManager authenticationManager,
|
||||
ILogger logger,
|
||||
IGlobalSettings globalSettings,
|
||||
IOwinRequest request)
|
||||
{
|
||||
if (logger == null) throw new ArgumentNullException("logger");
|
||||
if (request == null) throw new ArgumentNullException("request");
|
||||
_logger = logger;
|
||||
_request = request;
|
||||
_globalSettings = globalSettings;
|
||||
AuthenticationType = Constants.Security.BackOfficeAuthenticationType;
|
||||
_userManager = userManager ?? throw new ArgumentNullException(nameof(userManager));
|
||||
_claimsPrincipalFactory = claimsPrincipalFactory ?? throw new ArgumentNullException(nameof(claimsPrincipalFactory));
|
||||
_authenticationManager = authenticationManager ?? throw new ArgumentNullException(nameof(authenticationManager));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_globalSettings = globalSettings ?? throw new ArgumentNullException(nameof(globalSettings));
|
||||
_request = request ?? throw new ArgumentNullException(nameof(request));
|
||||
}
|
||||
|
||||
public override Task<ClaimsIdentity> CreateUserIdentityAsync(BackOfficeIdentityUser user)
|
||||
public async Task<ClaimsIdentity> CreateUserIdentityAsync(BackOfficeIdentityUser user)
|
||||
{
|
||||
return ((BackOfficeUserManager<BackOfficeIdentityUser>)UserManager).GenerateUserIdentityAsync(user);
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
|
||||
var claimsPrincipal = await _claimsPrincipalFactory.CreateAsync(user);
|
||||
return claimsPrincipal.Identity as ClaimsIdentity;
|
||||
}
|
||||
|
||||
public static BackOfficeSignInManager Create(IdentityFactoryOptions<BackOfficeSignInManager> options, IOwinContext context, IGlobalSettings globalSettings, ILogger logger)
|
||||
public static BackOfficeSignInManager Create(IOwinContext context, IGlobalSettings globalSettings, ILogger logger)
|
||||
{
|
||||
var userManager = context.GetBackOfficeUserManager();
|
||||
|
||||
return new BackOfficeSignInManager(
|
||||
context.GetBackOfficeUserManager(),
|
||||
userManager,
|
||||
new BackOfficeClaimsPrincipalFactory<BackOfficeIdentityUser>(userManager, new OptionsWrapper<IdentityOptions>(userManager.Options)),
|
||||
context.Authentication,
|
||||
logger,
|
||||
globalSettings,
|
||||
@@ -54,30 +70,33 @@ namespace Umbraco.Web.Security
|
||||
/// </summary>
|
||||
/// <param name="userName"/><param name="password"/><param name="isPersistent"/><param name="shouldLockout"/>
|
||||
/// <returns/>
|
||||
public override async Task<SignInStatus> PasswordSignInAsync(string userName, string password, bool isPersistent, bool shouldLockout)
|
||||
public async Task<SignInResult> PasswordSignInAsync(string userName, string password, bool isPersistent, bool shouldLockout)
|
||||
{
|
||||
var result = await PasswordSignInAsyncImpl(userName, password, isPersistent, shouldLockout);
|
||||
|
||||
switch (result)
|
||||
if (result.Succeeded)
|
||||
{
|
||||
case SignInStatus.Success:
|
||||
_logger.WriteCore(TraceEventType.Information, 0,
|
||||
$"User: {userName} logged in from IP address {_request.RemoteIpAddress}", null, null);
|
||||
break;
|
||||
case SignInStatus.LockedOut:
|
||||
_logger.WriteCore(TraceEventType.Information, 0,
|
||||
$"Login attempt failed for username {userName} from IP address {_request.RemoteIpAddress}, the user is locked", null, null);
|
||||
break;
|
||||
case SignInStatus.RequiresVerification:
|
||||
_logger.WriteCore(TraceEventType.Information, 0,
|
||||
$"Login attempt requires verification for username {userName} from IP address {_request.RemoteIpAddress}", null, null);
|
||||
break;
|
||||
case SignInStatus.Failure:
|
||||
_logger.WriteCore(TraceEventType.Information, 0,
|
||||
$"Login attempt failed for username {userName} from IP address {_request.RemoteIpAddress}", null, null);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
_logger.WriteCore(TraceEventType.Information, 0,
|
||||
$"User: {userName} logged in from IP address {_request.RemoteIpAddress}", null, null);
|
||||
}
|
||||
else if (result.IsLockedOut)
|
||||
{
|
||||
_logger.WriteCore(TraceEventType.Information, 0,
|
||||
$"Login attempt failed for username {userName} from IP address {_request.RemoteIpAddress}, the user is locked", null, null);
|
||||
}
|
||||
else if (result.RequiresTwoFactor)
|
||||
{
|
||||
_logger.WriteCore(TraceEventType.Information, 0,
|
||||
$"Login attempt requires verification for username {userName} from IP address {_request.RemoteIpAddress}", null, null);
|
||||
}
|
||||
else if (!result.Succeeded || result.IsNotAllowed)
|
||||
{
|
||||
_logger.WriteCore(TraceEventType.Information, 0,
|
||||
$"Login attempt failed for username {userName} from IP address {_request.RemoteIpAddress}", null, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -91,27 +110,21 @@ namespace Umbraco.Web.Security
|
||||
/// <param name="isPersistent"></param>
|
||||
/// <param name="shouldLockout"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<SignInStatus> PasswordSignInAsyncImpl(string userName, string password, bool isPersistent, bool shouldLockout)
|
||||
private async Task<SignInResult> PasswordSignInAsyncImpl(string userName, string password, bool isPersistent, bool shouldLockout)
|
||||
{
|
||||
if (UserManager == null)
|
||||
{
|
||||
return SignInStatus.Failure;
|
||||
}
|
||||
|
||||
var user = await UserManager.FindByNameAsync(userName);
|
||||
var user = await _userManager.FindByNameAsync(userName);
|
||||
|
||||
//if the user is null, create an empty one which can be used for auto-linking
|
||||
if (user == null)
|
||||
user = BackOfficeIdentityUser.CreateNew(_globalSettings, userName, null, _globalSettings.DefaultUILanguage);
|
||||
if (user == null) user = BackOfficeIdentityUser.CreateNew(_globalSettings, userName, null, _globalSettings.DefaultUILanguage);
|
||||
|
||||
//check the password for the user, this will allow a developer to auto-link
|
||||
//an account if they have specified an IBackOfficeUserPasswordChecker
|
||||
if (await UserManager.CheckPasswordAsync(user, password))
|
||||
if (await _userManager.CheckPasswordAsync(user, password))
|
||||
{
|
||||
//the underlying call to this will query the user by Id which IS cached!
|
||||
if (await UserManager.IsLockedOutAsync(user.Id))
|
||||
if (await _userManager.IsLockedOutAsync(user))
|
||||
{
|
||||
return SignInStatus.LockedOut;
|
||||
return SignInResult.LockedOut;
|
||||
}
|
||||
|
||||
// We need to verify that the user belongs to one or more groups that define content and media start nodes.
|
||||
@@ -125,11 +138,11 @@ namespace Umbraco.Web.Security
|
||||
$"Login attempt failed for username {userName} from IP address {_request.RemoteIpAddress}, no content and/or media start nodes could be found for any of the user's groups", null, null);
|
||||
|
||||
// We will say its a sucessful login which it is, but they have no node access
|
||||
return SignInStatus.Success;
|
||||
return SignInResult.Success;
|
||||
}
|
||||
}
|
||||
|
||||
await UserManager.ResetAccessFailedCountAsync(user.Id);
|
||||
await _userManager.ResetAccessFailedCountAsync(user);
|
||||
return await SignInOrTwoFactor(user, isPersistent);
|
||||
}
|
||||
|
||||
@@ -138,19 +151,18 @@ namespace Umbraco.Web.Security
|
||||
if (user.HasIdentity && shouldLockout)
|
||||
{
|
||||
// If lockout is requested, increment access failed count which might lock out the user
|
||||
await UserManager.AccessFailedAsync(user.Id);
|
||||
if (await UserManager.IsLockedOutAsync(user.Id))
|
||||
await _userManager.AccessFailedAsync(user);
|
||||
if (await _userManager.IsLockedOutAsync(user))
|
||||
{
|
||||
//at this point we've just locked the user out after too many failed login attempts
|
||||
|
||||
if (requestContext != null)
|
||||
{
|
||||
var backofficeUserManager = requestContext.GetBackOfficeUserManager();
|
||||
if (backofficeUserManager != null)
|
||||
backofficeUserManager.RaiseAccountLockedEvent(user.Id);
|
||||
if (backofficeUserManager != null) backofficeUserManager.RaiseAccountLockedEvent(user.Id);
|
||||
}
|
||||
|
||||
return SignInStatus.LockedOut;
|
||||
return SignInResult.LockedOut;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,7 +173,7 @@ namespace Umbraco.Web.Security
|
||||
backofficeUserManager.RaiseInvalidLoginAttemptEvent(userName);
|
||||
}
|
||||
|
||||
return SignInStatus.Failure;
|
||||
return SignInResult.Failed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -170,20 +182,20 @@ namespace Umbraco.Web.Security
|
||||
/// <param name="user"></param>
|
||||
/// <param name="isPersistent"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<SignInStatus> SignInOrTwoFactor(BackOfficeIdentityUser user, bool isPersistent)
|
||||
private async Task<SignInResult> SignInOrTwoFactor(BackOfficeIdentityUser user, bool isPersistent)
|
||||
{
|
||||
var id = Convert.ToString(user.Id);
|
||||
if (await UserManager.GetTwoFactorEnabledAsync(user.Id)
|
||||
&& (await UserManager.GetValidTwoFactorProvidersAsync(user.Id)).Count > 0)
|
||||
if (await _userManager.GetTwoFactorEnabledAsync(user)
|
||||
&& (await _userManager.GetValidTwoFactorProvidersAsync(user)).Count > 0)
|
||||
{
|
||||
var identity = new ClaimsIdentity(Constants.Security.BackOfficeTwoFactorAuthenticationType);
|
||||
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, id));
|
||||
identity.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, user.UserName));
|
||||
AuthenticationManager.SignIn(identity);
|
||||
return SignInStatus.RequiresVerification;
|
||||
_authenticationManager.SignIn(identity);
|
||||
return SignInResult.TwoFactorRequired;
|
||||
}
|
||||
await SignInAsync(user, isPersistent, false);
|
||||
return SignInStatus.Success;
|
||||
return SignInResult.Success;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -193,12 +205,12 @@ namespace Umbraco.Web.Security
|
||||
/// <param name="isPersistent"></param>
|
||||
/// <param name="rememberBrowser"></param>
|
||||
/// <returns></returns>
|
||||
public override async Task SignInAsync(BackOfficeIdentityUser user, bool isPersistent, bool rememberBrowser)
|
||||
public async Task SignInAsync(BackOfficeIdentityUser user, bool isPersistent, bool rememberBrowser)
|
||||
{
|
||||
var userIdentity = await CreateUserIdentityAsync(user);
|
||||
|
||||
// Clear any partial cookies from external or two factor partial sign ins
|
||||
AuthenticationManager.SignOut(
|
||||
_authenticationManager.SignOut(
|
||||
Constants.Security.BackOfficeExternalAuthenticationType,
|
||||
Constants.Security.BackOfficeTwoFactorAuthenticationType);
|
||||
|
||||
@@ -206,8 +218,8 @@ namespace Umbraco.Web.Security
|
||||
|
||||
if (rememberBrowser)
|
||||
{
|
||||
var rememberBrowserIdentity = AuthenticationManager.CreateTwoFactorRememberBrowserIdentity(ConvertIdToString(user.Id));
|
||||
AuthenticationManager.SignIn(new AuthenticationProperties()
|
||||
var rememberBrowserIdentity = _authenticationManager.CreateTwoFactorRememberBrowserIdentity(user.Id.ToString());
|
||||
_authenticationManager.SignIn(new AuthenticationProperties()
|
||||
{
|
||||
IsPersistent = isPersistent,
|
||||
AllowRefresh = true,
|
||||
@@ -217,7 +229,7 @@ namespace Umbraco.Web.Security
|
||||
}
|
||||
else
|
||||
{
|
||||
AuthenticationManager.SignIn(new AuthenticationProperties()
|
||||
_authenticationManager.SignIn(new AuthenticationProperties()
|
||||
{
|
||||
IsPersistent = isPersistent,
|
||||
AllowRefresh = true,
|
||||
@@ -231,7 +243,7 @@ namespace Umbraco.Web.Security
|
||||
if (user.AccessFailedCount > 0)
|
||||
//we have successfully logged in, reset the AccessFailedCount
|
||||
user.AccessFailedCount = 0;
|
||||
await UserManager.UpdateAsync(user);
|
||||
await _userManager.UpdateAsync(user);
|
||||
|
||||
//set the current request's principal to the identity just signed in!
|
||||
_request.User = new ClaimsPrincipal(userIdentity);
|
||||
@@ -250,14 +262,14 @@ namespace Umbraco.Web.Security
|
||||
/// <remarks>
|
||||
/// Replaces the underlying call which is not flexible and doesn't support a custom cookie
|
||||
/// </remarks>
|
||||
public new async Task<int> GetVerifiedUserIdAsync()
|
||||
public async Task<string> GetVerifiedUserIdAsync()
|
||||
{
|
||||
var result = await AuthenticationManager.AuthenticateAsync(Constants.Security.BackOfficeTwoFactorAuthenticationType);
|
||||
var result = await _authenticationManager.AuthenticateAsync(Constants.Security.BackOfficeTwoFactorAuthenticationType);
|
||||
if (result != null && result.Identity != null && string.IsNullOrEmpty(result.Identity.GetUserId()) == false)
|
||||
{
|
||||
return ConvertIdFromString(result.Identity.GetUserId());
|
||||
return result.Identity.GetUserId();
|
||||
}
|
||||
return int.MinValue;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -266,14 +278,14 @@ namespace Umbraco.Web.Security
|
||||
/// <returns></returns>
|
||||
public async Task<string> GetVerifiedUserNameAsync()
|
||||
{
|
||||
var result = await AuthenticationManager.AuthenticateAsync(Constants.Security.BackOfficeTwoFactorAuthenticationType);
|
||||
var result = await _authenticationManager.AuthenticateAsync(Constants.Security.BackOfficeTwoFactorAuthenticationType);
|
||||
if (result != null && result.Identity != null && string.IsNullOrEmpty(result.Identity.GetUserName()) == false)
|
||||
{
|
||||
return result.Identity.GetUserName();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Two factor verification step
|
||||
/// </summary>
|
||||
@@ -288,32 +300,33 @@ namespace Umbraco.Web.Security
|
||||
/// the default(int) value returned by the base class is always a valid user (i.e. the admin) so we just have to duplicate
|
||||
/// all of this code to check for int.MinValue
|
||||
/// </remarks>
|
||||
public override async Task<SignInStatus> TwoFactorSignInAsync(string provider, string code, bool isPersistent, bool rememberBrowser)
|
||||
public async Task<SignInResult> TwoFactorSignInAsync(string provider, string code, bool isPersistent, bool rememberBrowser)
|
||||
{
|
||||
var userId = await GetVerifiedUserIdAsync();
|
||||
if (userId == int.MinValue)
|
||||
if (string.IsNullOrWhiteSpace(userId))
|
||||
{
|
||||
return SignInStatus.Failure;
|
||||
return SignInResult.Failed;
|
||||
}
|
||||
var user = await UserManager.FindByIdAsync(userId);
|
||||
var user = await _userManager.FindByIdAsync(userId.ToString());
|
||||
if (user == null)
|
||||
{
|
||||
return SignInStatus.Failure;
|
||||
return SignInResult.Failed;
|
||||
}
|
||||
if (await UserManager.IsLockedOutAsync(user.Id))
|
||||
if (await _userManager.IsLockedOutAsync(user))
|
||||
{
|
||||
return SignInStatus.LockedOut;
|
||||
return SignInResult.LockedOut;
|
||||
}
|
||||
if (await UserManager.VerifyTwoFactorTokenAsync(user.Id, provider, code))
|
||||
if (await _userManager.VerifyTwoFactorTokenAsync(user, provider, code))
|
||||
{
|
||||
// When token is verified correctly, clear the access failed count used for lockout
|
||||
await UserManager.ResetAccessFailedCountAsync(user.Id);
|
||||
await _userManager.ResetAccessFailedCountAsync(user);
|
||||
await SignInAsync(user, isPersistent, rememberBrowser);
|
||||
return SignInStatus.Success;
|
||||
return SignInResult.Success;
|
||||
}
|
||||
|
||||
// If the token is incorrect, record the failure which also may cause the user to be locked out
|
||||
await UserManager.AccessFailedAsync(user.Id);
|
||||
return SignInStatus.Failure;
|
||||
await _userManager.AccessFailedAsync(user);
|
||||
return SignInResult.Failed;
|
||||
}
|
||||
|
||||
/// <summary>Send a two factor code to a user</summary>
|
||||
@@ -325,15 +338,14 @@ namespace Umbraco.Web.Security
|
||||
/// the default(int) value returned by the base class is always a valid user (i.e. the admin) so we just have to duplicate
|
||||
/// all of this code to check for int.MinVale instead.
|
||||
/// </remarks>
|
||||
public override async Task<bool> SendTwoFactorCodeAsync(string provider)
|
||||
public Task<bool> SendTwoFactorCodeAsync(string provider)
|
||||
{
|
||||
var userId = await GetVerifiedUserIdAsync();
|
||||
if (userId == int.MinValue)
|
||||
return false;
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
var token = await UserManager.GenerateTwoFactorTokenAsync(userId, provider);
|
||||
var identityResult = await UserManager.NotifyTwoFactorTokenAsync(userId, provider, token);
|
||||
return identityResult.Succeeded;
|
||||
public void Dispose()
|
||||
{
|
||||
_userManager?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,218 +1,187 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Identity;
|
||||
using Microsoft.AspNet.Identity.Owin;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Owin.Security.DataProtection;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Configuration.UmbracoSettings;
|
||||
using Umbraco.Core.Mapping;
|
||||
using Umbraco.Core.Models.Identity;
|
||||
using Umbraco.Net;
|
||||
using Umbraco.Core.Security;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Net;
|
||||
using Umbraco.Web.Models.Identity;
|
||||
using IPasswordHasher = Microsoft.AspNet.Identity.IPasswordHasher;
|
||||
|
||||
namespace Umbraco.Web.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// Default back office user manager
|
||||
/// </summary>
|
||||
public class BackOfficeUserManager : BackOfficeUserManager<BackOfficeIdentityUser>
|
||||
{
|
||||
public const string OwinMarkerKey = "Umbraco.Web.Security.Identity.BackOfficeUserManagerMarker";
|
||||
|
||||
public BackOfficeUserManager(
|
||||
IUserStore<BackOfficeIdentityUser, int> store,
|
||||
IdentityFactoryOptions<BackOfficeUserManager> options,
|
||||
IContentSettings contentSettingsConfig,
|
||||
IPasswordConfiguration passwordConfiguration,
|
||||
IIpResolver ipResolver,
|
||||
IGlobalSettings globalSettings)
|
||||
: base(store, passwordConfiguration, ipResolver)
|
||||
IUserStore<BackOfficeIdentityUser> store,
|
||||
IOptions<IdentityOptions> optionsAccessor,
|
||||
IEnumerable<IUserValidator<BackOfficeIdentityUser>> userValidators,
|
||||
IEnumerable<IPasswordValidator<BackOfficeIdentityUser>> passwordValidators,
|
||||
ILookupNormalizer keyNormalizer,
|
||||
IdentityErrorDescriber errors,
|
||||
IDataProtectionProvider dataProtectionProvider,
|
||||
ILogger<UserManager<BackOfficeIdentityUser>> logger)
|
||||
: base(passwordConfiguration, ipResolver, store, optionsAccessor, userValidators, passwordValidators, keyNormalizer, errors, null, logger)
|
||||
{
|
||||
if (options == null) throw new ArgumentNullException("options");
|
||||
InitUserManager(this, passwordConfiguration, options.DataProtectionProvider, contentSettingsConfig, globalSettings);
|
||||
InitUserManager(this, dataProtectionProvider);
|
||||
}
|
||||
|
||||
|
||||
#region Static Create methods
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates a BackOfficeUserManager instance with all default options and the default BackOfficeUserManager
|
||||
/// </summary>
|
||||
/// <param name="options"></param>
|
||||
/// <param name="userService"></param>
|
||||
/// <param name="entityService"></param>
|
||||
/// <param name="externalLoginService"></param>
|
||||
/// <param name="passwordConfiguration"></param>
|
||||
/// <param name="contentSettingsConfig"></param>
|
||||
/// <param name="globalSettings"></param>
|
||||
/// <returns></returns>
|
||||
public static BackOfficeUserManager Create(
|
||||
IdentityFactoryOptions<BackOfficeUserManager> options,
|
||||
IUserService userService,
|
||||
IEntityService entityService,
|
||||
IExternalLoginService externalLoginService,
|
||||
UmbracoMapper mapper,
|
||||
IContentSettings contentSettingsConfig,
|
||||
IGlobalSettings globalSettings,
|
||||
UmbracoMapper mapper,
|
||||
IPasswordConfiguration passwordConfiguration,
|
||||
IIpResolver ipResolver)
|
||||
IIpResolver ipResolver,
|
||||
IdentityErrorDescriber errors,
|
||||
IDataProtectionProvider dataProtectionProvider,
|
||||
ILogger<UserManager<BackOfficeIdentityUser>> logger)
|
||||
{
|
||||
if (options == null) throw new ArgumentNullException("options");
|
||||
if (userService == null) throw new ArgumentNullException("userService");
|
||||
if (externalLoginService == null) throw new ArgumentNullException("externalLoginService");
|
||||
|
||||
var store = new BackOfficeUserStore(userService, entityService, externalLoginService, globalSettings, mapper);
|
||||
var manager = new BackOfficeUserManager(store, options, contentSettingsConfig, passwordConfiguration, ipResolver, globalSettings);
|
||||
return manager;
|
||||
|
||||
return Create(
|
||||
passwordConfiguration,
|
||||
ipResolver,
|
||||
store,
|
||||
errors,
|
||||
dataProtectionProvider,
|
||||
logger);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a BackOfficeUserManager instance with all default options and a custom BackOfficeUserManager instance
|
||||
/// </summary>
|
||||
/// <param name="options"></param>
|
||||
/// <param name="customUserStore"></param>
|
||||
/// <param name="passwordConfiguration"></param>
|
||||
/// <param name="contentSettingsConfig"></param>
|
||||
/// <returns></returns>
|
||||
public static BackOfficeUserManager Create(
|
||||
IdentityFactoryOptions<BackOfficeUserManager> options,
|
||||
BackOfficeUserStore customUserStore,
|
||||
IContentSettings contentSettingsConfig,
|
||||
IPasswordConfiguration passwordConfiguration,
|
||||
IIpResolver ipResolver,
|
||||
IGlobalSettings globalSettings)
|
||||
IUserStore<BackOfficeIdentityUser> customUserStore,
|
||||
IdentityErrorDescriber errors,
|
||||
IDataProtectionProvider dataProtectionProvider,
|
||||
ILogger<UserManager<BackOfficeIdentityUser>> logger)
|
||||
{
|
||||
var manager = new BackOfficeUserManager(customUserStore, options, contentSettingsConfig, passwordConfiguration, ipResolver, globalSettings);
|
||||
return manager;
|
||||
var options = new IdentityOptions();
|
||||
|
||||
// Configure validation logic for usernames
|
||||
var userValidators = new List<UserValidator<BackOfficeIdentityUser>> { new BackOfficeUserValidator<BackOfficeIdentityUser>() };
|
||||
options.User.RequireUniqueEmail = true;
|
||||
|
||||
// Configure validation logic for passwords
|
||||
var passwordValidators = new List<IPasswordValidator<BackOfficeIdentityUser>> { new PasswordValidator<BackOfficeIdentityUser>() };
|
||||
options.Password.RequiredLength = passwordConfiguration.RequiredLength;
|
||||
options.Password.RequireNonAlphanumeric = passwordConfiguration.RequireNonLetterOrDigit;
|
||||
options.Password.RequireDigit = passwordConfiguration.RequireDigit;
|
||||
options.Password.RequireLowercase = passwordConfiguration.RequireLowercase;
|
||||
options.Password.RequireUppercase = passwordConfiguration.RequireUppercase;
|
||||
|
||||
// Ensure Umbraco security stamp claim type is used
|
||||
options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier;
|
||||
options.ClaimsIdentity.UserNameClaimType = ClaimTypes.Name;
|
||||
options.ClaimsIdentity.RoleClaimType = ClaimTypes.Role;
|
||||
options.ClaimsIdentity.SecurityStampClaimType = Constants.Web.SecurityStampClaimType;
|
||||
|
||||
options.Lockout.AllowedForNewUsers = true;
|
||||
options.Lockout.MaxFailedAccessAttempts = passwordConfiguration.MaxFailedAccessAttemptsBeforeLockout;
|
||||
//NOTE: This just needs to be in the future, we currently don't support a lockout timespan, it's either they are locked
|
||||
// or they are not locked, but this determines what is set on the account lockout date which corresponds to whether they are
|
||||
// locked out or not.
|
||||
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromDays(30);
|
||||
|
||||
return new BackOfficeUserManager(
|
||||
passwordConfiguration,
|
||||
ipResolver,
|
||||
customUserStore,
|
||||
new OptionsWrapper<IdentityOptions>(options),
|
||||
userValidators,
|
||||
passwordValidators,
|
||||
new NopLookupNormalizer(),
|
||||
errors,
|
||||
dataProtectionProvider,
|
||||
logger);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic Back office user manager
|
||||
/// </summary>
|
||||
public class BackOfficeUserManager<T> : UserManager<T, int>
|
||||
public class BackOfficeUserManager<T> : UserManager<T>
|
||||
where T : BackOfficeIdentityUser
|
||||
{
|
||||
private PasswordGenerator _passwordGenerator;
|
||||
|
||||
public BackOfficeUserManager(IUserStore<T, int> store,
|
||||
public BackOfficeUserManager(
|
||||
IPasswordConfiguration passwordConfiguration,
|
||||
IIpResolver ipResolver)
|
||||
: base(store)
|
||||
IIpResolver ipResolver,
|
||||
IUserStore<T> store,
|
||||
IOptions<IdentityOptions> optionsAccessor,
|
||||
IEnumerable<IUserValidator<T>> userValidators,
|
||||
IEnumerable<IPasswordValidator<T>> passwordValidators,
|
||||
ILookupNormalizer keyNormalizer,
|
||||
IdentityErrorDescriber errors,
|
||||
IServiceProvider services,
|
||||
ILogger<UserManager<T>> logger)
|
||||
: base(store, optionsAccessor, null, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
|
||||
{
|
||||
PasswordConfiguration = passwordConfiguration;
|
||||
IpResolver = ipResolver;
|
||||
PasswordConfiguration = passwordConfiguration ?? throw new ArgumentNullException(nameof(passwordConfiguration));
|
||||
IpResolver = ipResolver ?? throw new ArgumentNullException(nameof(ipResolver));
|
||||
}
|
||||
|
||||
#region What we support do not currently
|
||||
|
||||
#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
|
||||
public override bool SupportsUserClaim
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
|
||||
public override bool SupportsUserClaim => false;
|
||||
|
||||
// TODO: Support this
|
||||
public override bool SupportsQueryableUsers
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
public override bool SupportsQueryableUsers => false;
|
||||
|
||||
/// <summary>
|
||||
/// Developers will need to override this to support custom 2 factor auth
|
||||
/// </summary>
|
||||
public override bool SupportsUserTwoFactor
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
public override bool SupportsUserTwoFactor => false;
|
||||
|
||||
// TODO: Support this
|
||||
public override bool SupportsUserPhoneNumber
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
public override bool SupportsUserPhoneNumber => false;
|
||||
#endregion
|
||||
|
||||
public virtual async Task<ClaimsIdentity> GenerateUserIdentityAsync(T user)
|
||||
{
|
||||
// NOTE the authenticationType must match the umbraco one
|
||||
// defined in CookieAuthenticationOptions.AuthenticationType
|
||||
var userIdentity = await CreateIdentityAsync(user, Core.Constants.Security.BackOfficeAuthenticationType);
|
||||
return userIdentity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the user manager with the correct options
|
||||
/// </summary>
|
||||
/// <param name="manager"></param>
|
||||
/// <param name="passwordConfig"></param>
|
||||
/// <param name="dataProtectionProvider"></param>
|
||||
/// <param name="contentSettingsConfig"></param>
|
||||
/// <returns></returns>
|
||||
protected void InitUserManager(
|
||||
BackOfficeUserManager<T> manager,
|
||||
IPasswordConfiguration passwordConfig,
|
||||
IDataProtectionProvider dataProtectionProvider,
|
||||
IContentSettings contentSettingsConfig,
|
||||
IGlobalSettings globalSettings)
|
||||
IDataProtectionProvider dataProtectionProvider)
|
||||
{
|
||||
// Configure validation logic for usernames
|
||||
manager.UserValidator = new BackOfficeUserValidator<T>(manager)
|
||||
{
|
||||
AllowOnlyAlphanumericUserNames = false,
|
||||
RequireUniqueEmail = true
|
||||
};
|
||||
|
||||
// Configure validation logic for passwords
|
||||
manager.PasswordValidator = new ConfiguredPasswordValidator(passwordConfig);
|
||||
|
||||
//use a custom hasher based on our membership provider
|
||||
manager.PasswordHasher = GetDefaultPasswordHasher(passwordConfig);
|
||||
// use a custom hasher based on our membership provider
|
||||
PasswordHasher = GetDefaultPasswordHasher(PasswordConfiguration);
|
||||
|
||||
// set OWIN data protection token provider as default
|
||||
if (dataProtectionProvider != null)
|
||||
{
|
||||
manager.UserTokenProvider = new DataProtectorTokenProvider<T, int>(dataProtectionProvider.Create("ASP.NET Identity"))
|
||||
{
|
||||
TokenLifespan = TimeSpan.FromDays(3)
|
||||
};
|
||||
manager.RegisterTokenProvider(
|
||||
TokenOptions.DefaultProvider,
|
||||
new OwinDataProtectorTokenProvider<T>(dataProtectionProvider.Create("ASP.NET Identity"))
|
||||
{
|
||||
TokenLifespan = TimeSpan.FromDays(3)
|
||||
});
|
||||
}
|
||||
|
||||
manager.UserLockoutEnabledByDefault = true;
|
||||
manager.MaxFailedAccessAttemptsBeforeLockout = passwordConfig.MaxFailedAccessAttemptsBeforeLockout;
|
||||
//NOTE: This just needs to be in the future, we currently don't support a lockout timespan, it's either they are locked
|
||||
// or they are not locked, but this determines what is set on the account lockout date which corresponds to whether they are
|
||||
// locked out or not.
|
||||
manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromDays(30);
|
||||
|
||||
//custom identity factory for creating the identity object for which we auth against in the back office
|
||||
manager.ClaimsIdentityFactory = new BackOfficeClaimsIdentityFactory<T>();
|
||||
|
||||
manager.EmailService = new EmailService(
|
||||
contentSettingsConfig.NotificationEmailAddress,
|
||||
new EmailSender(globalSettings));
|
||||
|
||||
//NOTE: Not implementing these, if people need custom 2 factor auth, they'll need to implement their own UserStore to support it
|
||||
|
||||
//// Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user
|
||||
//// You can write your own provider and plug in here.
|
||||
//manager.RegisterTwoFactorProvider("PhoneCode", new PhoneNumberTokenProvider<ApplicationUser>
|
||||
//{
|
||||
// MessageFormat = "Your security code is: {0}"
|
||||
//});
|
||||
//manager.RegisterTwoFactorProvider("EmailCode", new EmailTokenProvider<ApplicationUser>
|
||||
//{
|
||||
// Subject = "Security Code",
|
||||
// BodyFormat = "Your security code is: {0}"
|
||||
//});
|
||||
|
||||
//manager.SmsService = new SmsService();
|
||||
// register ASP.NET Core Identity token providers
|
||||
manager.RegisterTokenProvider(TokenOptions.DefaultEmailProvider, new EmailTokenProvider<T>());
|
||||
manager.RegisterTokenProvider(TokenOptions.DefaultPhoneProvider, new PhoneNumberTokenProvider<T>());
|
||||
manager.RegisterTokenProvider(TokenOptions.DefaultAuthenticatorProvider, new AuthenticatorTokenProvider<T>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -221,12 +190,11 @@ namespace Umbraco.Web.Security
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="sessionId"></param>
|
||||
/// <returns></returns>
|
||||
public virtual async Task<bool> ValidateSessionIdAsync(int userId, string sessionId)
|
||||
public virtual async Task<bool> ValidateSessionIdAsync(string userId, string sessionId)
|
||||
{
|
||||
var userSessionStore = Store as IUserSessionStore<BackOfficeIdentityUser, int>;
|
||||
var userSessionStore = Store as IUserSessionStore<T>;
|
||||
//if this is not set, for backwards compat (which would be super rare), we'll just approve it
|
||||
if (userSessionStore == null)
|
||||
return true;
|
||||
if (userSessionStore == null) return true;
|
||||
|
||||
return await userSessionStore.ValidateSessionIdAsync(userId, sessionId);
|
||||
}
|
||||
@@ -235,12 +203,12 @@ namespace Umbraco.Web.Security
|
||||
/// This will determine which password hasher to use based on what is defined in config
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected virtual IPasswordHasher GetDefaultPasswordHasher(IPasswordConfiguration passwordConfiguration)
|
||||
protected virtual IPasswordHasher<T> GetDefaultPasswordHasher(IPasswordConfiguration passwordConfiguration)
|
||||
{
|
||||
//we can use the user aware password hasher (which will be the default and preferred way)
|
||||
return new UserAwarePasswordHasher(new PasswordSecurity(passwordConfiguration));
|
||||
return new UserAwarePasswordHasher<T>(new PasswordSecurity(passwordConfiguration));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets/sets the default back office user password checker
|
||||
/// </summary>
|
||||
@@ -262,20 +230,18 @@ namespace Umbraco.Web.Security
|
||||
/// <summary>
|
||||
/// Override to check the user approval value as well as the user lock out date, by default this only checks the user's locked out date
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="user"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// In the ASP.NET Identity world, there is only one value for being locked out, in Umbraco we have 2 so when checking this for Umbraco we need to check both values
|
||||
/// </remarks>
|
||||
public override async Task<bool> IsLockedOutAsync(int userId)
|
||||
public override async Task<bool> IsLockedOutAsync(T user)
|
||||
{
|
||||
var user = await FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
throw new InvalidOperationException("No user found by id " + userId);
|
||||
if (user.IsApproved == false)
|
||||
return true;
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
|
||||
return await base.IsLockedOutAsync(userId);
|
||||
if (user.IsApproved == false) return true;
|
||||
|
||||
return await base.IsLockedOutAsync(user);
|
||||
}
|
||||
|
||||
#region Overrides for password logic
|
||||
@@ -325,13 +291,7 @@ namespace Umbraco.Web.Security
|
||||
//use the default behavior
|
||||
return await base.CheckPasswordAsync(user, password);
|
||||
}
|
||||
|
||||
public override Task<IdentityResult> ResetPasswordAsync(int userId, string token, string newPassword)
|
||||
{
|
||||
var result = base.ResetPasswordAsync(userId, token, newPassword);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// This is a special method that will reset the password but will raise the Password Changed event instead of the reset event
|
||||
/// </summary>
|
||||
@@ -343,65 +303,53 @@ namespace Umbraco.Web.Security
|
||||
/// We use this because in the back office the only way an admin can change another user's password without first knowing their password
|
||||
/// is to generate a token and reset it, however, when we do this we want to track a password change, not a password reset
|
||||
/// </remarks>
|
||||
public Task<IdentityResult> ChangePasswordWithResetAsync(int userId, string token, string newPassword)
|
||||
public async Task<IdentityResult> ChangePasswordWithResetAsync(int userId, string token, string newPassword)
|
||||
{
|
||||
var result = base.ResetPasswordAsync(userId, token, newPassword);
|
||||
if (result.Result.Succeeded)
|
||||
RaisePasswordChangedEvent(userId);
|
||||
var user = await base.FindByIdAsync(userId.ToString());
|
||||
if (user == null) throw new InvalidOperationException("Could not find user");
|
||||
|
||||
var result = await base.ResetPasswordAsync(user, token, newPassword);
|
||||
if (result.Succeeded) RaisePasswordChangedEvent(userId);
|
||||
return result;
|
||||
}
|
||||
|
||||
public override Task<IdentityResult> ChangePasswordAsync(int userId, string currentPassword, string newPassword)
|
||||
public override async Task<IdentityResult> ChangePasswordAsync(T user, string currentPassword, string newPassword)
|
||||
{
|
||||
var result = base.ChangePasswordAsync(userId, currentPassword, newPassword);
|
||||
if (result.Result.Succeeded)
|
||||
RaisePasswordChangedEvent(userId);
|
||||
var result = await base.ChangePasswordAsync(user, currentPassword, newPassword);
|
||||
if (result.Succeeded) RaisePasswordChangedEvent(user.Id);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Override to determine how to hash the password
|
||||
/// </summary>
|
||||
/// <param name="store"></param>
|
||||
/// <param name="user"></param>
|
||||
/// <param name="password"></param>
|
||||
/// <returns></returns>
|
||||
protected override async Task<bool> VerifyPasswordAsync(IUserPasswordStore<T, int> store, T user, string password)
|
||||
{
|
||||
var userAwarePasswordHasher = PasswordHasher as IUserAwarePasswordHasher<BackOfficeIdentityUser, int>;
|
||||
if (userAwarePasswordHasher == null)
|
||||
return await base.VerifyPasswordAsync(store, user, password);
|
||||
|
||||
var hash = await store.GetPasswordHashAsync(user);
|
||||
return userAwarePasswordHasher.VerifyHashedPassword(user, hash, password) != PasswordVerificationResult.Failed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override to determine how to hash the password
|
||||
/// </summary>
|
||||
/// <param name="passwordStore"></param>
|
||||
/// <param name="user"></param>
|
||||
/// <param name="newPassword"></param>
|
||||
/// <param name="validatePassword"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// This method is called anytime the password needs to be hashed for storage (i.e. including when reset password is used)
|
||||
/// </remarks>
|
||||
protected override async Task<IdentityResult> UpdatePassword(IUserPasswordStore<T, int> passwordStore, T user, string newPassword)
|
||||
protected override async Task<IdentityResult> UpdatePasswordHash(T user, string newPassword, bool validatePassword)
|
||||
{
|
||||
user.LastPasswordChangeDateUtc = DateTime.UtcNow;
|
||||
var userAwarePasswordHasher = PasswordHasher as IUserAwarePasswordHasher<BackOfficeIdentityUser, int>;
|
||||
if (userAwarePasswordHasher == null)
|
||||
return await base.UpdatePassword(passwordStore, user, newPassword);
|
||||
|
||||
var result = await PasswordValidator.ValidateAsync(newPassword);
|
||||
if (result.Succeeded == false)
|
||||
return result;
|
||||
if (validatePassword)
|
||||
{
|
||||
var validate = await ValidatePasswordAsync(user, newPassword);
|
||||
if (!validate.Succeeded)
|
||||
{
|
||||
return validate;
|
||||
}
|
||||
}
|
||||
|
||||
await passwordStore.SetPasswordHashAsync(user, userAwarePasswordHasher.HashPassword(user, newPassword));
|
||||
var passwordStore = Store as IUserPasswordStore<T>;
|
||||
if (passwordStore == null) throw new NotSupportedException("The current user store does not implement " + typeof(IUserPasswordStore<>));
|
||||
|
||||
var hash = newPassword != null ? PasswordHasher.HashPassword(user, newPassword) : null;
|
||||
await passwordStore.SetPasswordHashAsync(user, hash, CancellationToken);
|
||||
await UpdateSecurityStampInternal(user);
|
||||
return IdentityResult.Success;
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -409,22 +357,20 @@ namespace Umbraco.Web.Security
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <returns></returns>
|
||||
private async Task UpdateSecurityStampInternal(BackOfficeIdentityUser user)
|
||||
private async Task UpdateSecurityStampInternal(T user)
|
||||
{
|
||||
if (SupportsUserSecurityStamp == false)
|
||||
return;
|
||||
await GetSecurityStore().SetSecurityStampAsync(user, NewSecurityStamp());
|
||||
if (SupportsUserSecurityStamp == false) return;
|
||||
await GetSecurityStore().SetSecurityStampAsync(user, NewSecurityStamp(), CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is copied from the underlying .NET base class since they decided to not expose it
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private IUserSecurityStampStore<BackOfficeIdentityUser, int> GetSecurityStore()
|
||||
private IUserSecurityStampStore<T> GetSecurityStore()
|
||||
{
|
||||
var store = Store as IUserSecurityStampStore<BackOfficeIdentityUser, int>;
|
||||
if (store == null)
|
||||
throw new NotSupportedException("The current user store does not implement " + typeof(IUserSecurityStampStore<>));
|
||||
var store = Store as IUserSecurityStampStore<T>;
|
||||
if (store == null) throw new NotSupportedException("The current user store does not implement " + typeof(IUserSecurityStampStore<>));
|
||||
return store;
|
||||
}
|
||||
|
||||
@@ -439,68 +385,66 @@ namespace Umbraco.Web.Security
|
||||
|
||||
#endregion
|
||||
|
||||
public override async Task<IdentityResult> SetLockoutEndDateAsync(int userId, DateTimeOffset lockoutEnd)
|
||||
public override async Task<IdentityResult> SetLockoutEndDateAsync(T user, DateTimeOffset? lockoutEnd)
|
||||
{
|
||||
var result = await base.SetLockoutEndDateAsync(userId, lockoutEnd);
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
|
||||
var result = await base.SetLockoutEndDateAsync(user, lockoutEnd);
|
||||
|
||||
// The way we unlock is by setting the lockoutEnd date to the current datetime
|
||||
if (result.Succeeded && lockoutEnd >= DateTimeOffset.UtcNow)
|
||||
{
|
||||
RaiseAccountLockedEvent(userId);
|
||||
RaiseAccountLockedEvent(user.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
RaiseAccountUnlockedEvent(userId);
|
||||
RaiseAccountUnlockedEvent(user.Id);
|
||||
//Resets the login attempt fails back to 0 when unlock is clicked
|
||||
await ResetAccessFailedCountAsync(userId);
|
||||
|
||||
await ResetAccessFailedCountAsync(user);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public override async Task<IdentityResult> ResetAccessFailedCountAsync(int userId)
|
||||
public override async Task<IdentityResult> ResetAccessFailedCountAsync(T user)
|
||||
{
|
||||
var lockoutStore = (IUserLockoutStore<BackOfficeIdentityUser, int>)Store;
|
||||
var user = await FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
throw new InvalidOperationException("No user found by user id " + userId);
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
|
||||
var accessFailedCount = await GetAccessFailedCountAsync(user.Id);
|
||||
var lockoutStore = (IUserLockoutStore<T>)Store;
|
||||
var accessFailedCount = await GetAccessFailedCountAsync(user);
|
||||
|
||||
if (accessFailedCount == 0)
|
||||
return IdentityResult.Success;
|
||||
|
||||
await lockoutStore.ResetAccessFailedCountAsync(user);
|
||||
await lockoutStore.ResetAccessFailedCountAsync(user, CancellationToken.None);
|
||||
//raise the event now that it's reset
|
||||
RaiseResetAccessFailedCountEvent(userId);
|
||||
RaiseResetAccessFailedCountEvent(user.Id);
|
||||
return await UpdateAsync(user);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the Microsoft ASP.NET user management method
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="user"></param>
|
||||
/// <returns>
|
||||
/// returns a Async Task<IdentityResult>
|
||||
/// returns a Async Task<IdentityResult />
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// Doesn't set fail attempts back to 0
|
||||
/// </remarks>
|
||||
public override async Task<IdentityResult> AccessFailedAsync(int userId)
|
||||
public override async Task<IdentityResult> AccessFailedAsync(T user)
|
||||
{
|
||||
var lockoutStore = (IUserLockoutStore<BackOfficeIdentityUser, int>)Store;
|
||||
var user = await FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
throw new InvalidOperationException("No user found by user id " + userId);
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
|
||||
var count = await lockoutStore.IncrementAccessFailedCountAsync(user);
|
||||
var lockoutStore = Store as IUserLockoutStore<T>;
|
||||
if (lockoutStore == null) throw new NotSupportedException("The current user store does not implement " + typeof(IUserLockoutStore<>));
|
||||
|
||||
if (count >= MaxFailedAccessAttemptsBeforeLockout)
|
||||
var count = await lockoutStore.IncrementAccessFailedCountAsync(user, CancellationToken.None);
|
||||
|
||||
if (count >= Options.Lockout.MaxFailedAccessAttempts)
|
||||
{
|
||||
await lockoutStore.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.Add(DefaultAccountLockoutTimeSpan));
|
||||
await lockoutStore.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.Add(Options.Lockout.DefaultLockoutTimeSpan),
|
||||
CancellationToken.None);
|
||||
//NOTE: in normal aspnet identity this would do set the number of failed attempts back to 0
|
||||
//here we are persisting the value for the back office
|
||||
}
|
||||
@@ -508,8 +452,7 @@ namespace Umbraco.Web.Security
|
||||
var result = await UpdateAsync(user);
|
||||
|
||||
//Slightly confusing: this will return a Success if we successfully update the AccessFailed count
|
||||
if (result.Succeeded)
|
||||
RaiseLoginFailedEvent(userId);
|
||||
if (result.Succeeded) RaiseLoginFailedEvent(user.Id);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -635,7 +578,5 @@ namespace Umbraco.Web.Security
|
||||
{
|
||||
if (ResetAccessFailedCount != null) ResetAccessFailedCount(this, e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
using System;
|
||||
using Microsoft.AspNet.Identity.Owin;
|
||||
using Microsoft.Owin;
|
||||
using Umbraco.Core.Models.Identity;
|
||||
using Umbraco.Core.Security;
|
||||
using Umbraco.Web.Models.Identity;
|
||||
|
||||
namespace Umbraco.Web.Security
|
||||
|
||||
@@ -2,30 +2,30 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Identity;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Mapping;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.Identity;
|
||||
using Umbraco.Core.Models.Membership;
|
||||
using Umbraco.Core.Security;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Web.Models.Identity;
|
||||
using IUser = Umbraco.Core.Models.Membership.IUser;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace Umbraco.Core.Security
|
||||
namespace Umbraco.Web.Security
|
||||
{
|
||||
public class BackOfficeUserStore : DisposableObjectSlim,
|
||||
IUserStore<BackOfficeIdentityUser, int>,
|
||||
IUserPasswordStore<BackOfficeIdentityUser, int>,
|
||||
IUserEmailStore<BackOfficeIdentityUser, int>,
|
||||
IUserLoginStore<BackOfficeIdentityUser, int>,
|
||||
IUserRoleStore<BackOfficeIdentityUser, int>,
|
||||
IUserSecurityStampStore<BackOfficeIdentityUser, int>,
|
||||
IUserLockoutStore<BackOfficeIdentityUser, int>,
|
||||
IUserTwoFactorStore<BackOfficeIdentityUser, int>,
|
||||
IUserSessionStore<BackOfficeIdentityUser, int>
|
||||
IUserPasswordStore<BackOfficeIdentityUser>,
|
||||
IUserEmailStore<BackOfficeIdentityUser>,
|
||||
IUserLoginStore<BackOfficeIdentityUser>,
|
||||
IUserRoleStore<BackOfficeIdentityUser>,
|
||||
IUserSecurityStampStore<BackOfficeIdentityUser>,
|
||||
IUserLockoutStore<BackOfficeIdentityUser>,
|
||||
IUserTwoFactorStore<BackOfficeIdentityUser>,
|
||||
IUserSessionStore<BackOfficeIdentityUser>
|
||||
|
||||
// TODO: This would require additional columns/tables for now people will need to implement this on their own
|
||||
//IUserPhoneNumberStore<BackOfficeIdentityUser, int>,
|
||||
@@ -61,13 +61,53 @@ namespace Umbraco.Core.Security
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
public Task<string> GetUserIdAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
|
||||
return Task.FromResult(user.Id.ToString());
|
||||
}
|
||||
|
||||
public Task<string> GetUserNameAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
|
||||
return Task.FromResult(user.UserName);
|
||||
}
|
||||
|
||||
public Task SetUserNameAsync(BackOfficeIdentityUser user, string userName, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
|
||||
user.UserName = userName;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<string> GetNormalizedUserNameAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken)
|
||||
{
|
||||
return GetUserNameAsync(user, cancellationToken);
|
||||
}
|
||||
|
||||
public Task SetNormalizedUserNameAsync(BackOfficeIdentityUser user, string normalizedName, CancellationToken cancellationToken)
|
||||
{
|
||||
return SetUserNameAsync(user, normalizedName, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Insert a new user
|
||||
/// </summary>
|
||||
/// <param name="user"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns/>
|
||||
public Task CreateAsync(BackOfficeIdentityUser user)
|
||||
public Task<IdentityResult> CreateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
|
||||
@@ -75,9 +115,9 @@ namespace Umbraco.Core.Security
|
||||
// with an external provider so we'll just generate one and prefix it, the
|
||||
// prefix will help us determine if the password hasn't actually been specified yet.
|
||||
//this will hash the guid with a salt so should be nicely random
|
||||
var aspHasher = new PasswordHasher();
|
||||
var aspHasher = new PasswordHasher<BackOfficeIdentityUser>();
|
||||
var emptyPasswordValue = Constants.Security.EmptyPasswordPrefix +
|
||||
aspHasher.HashPassword(Guid.NewGuid().ToString("N"));
|
||||
aspHasher.HashPassword(user, Guid.NewGuid().ToString("N"));
|
||||
|
||||
var userEntity = new User(_globalSettings, user.Name, user.Email, user.UserName, emptyPasswordValue)
|
||||
{
|
||||
@@ -98,26 +138,22 @@ namespace Umbraco.Core.Security
|
||||
//re-assign id
|
||||
user.Id = userEntity.Id;
|
||||
|
||||
return Task.FromResult(0);
|
||||
return Task.FromResult(IdentityResult.Success);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update a user
|
||||
/// </summary>
|
||||
/// <param name="user"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns/>
|
||||
public async Task UpdateAsync(BackOfficeIdentityUser user)
|
||||
public async Task<IdentityResult> UpdateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
|
||||
var asInt = user.Id.TryConvertTo<int>();
|
||||
if (asInt == false)
|
||||
{
|
||||
throw new InvalidOperationException("The user id must be an integer to work with the Umbraco");
|
||||
}
|
||||
|
||||
var found = _userService.GetUserById(asInt.Result);
|
||||
var found = _userService.GetUserById(user.Id);
|
||||
if (found != null)
|
||||
{
|
||||
// we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it.
|
||||
@@ -134,6 +170,8 @@ namespace Umbraco.Core.Security
|
||||
_externalLoginService.SaveUserLogins(found.Id, logins.Select(UserLoginInfoWrapper.Wrap));
|
||||
}
|
||||
}
|
||||
|
||||
return IdentityResult.Success;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -141,40 +179,35 @@ namespace Umbraco.Core.Security
|
||||
/// </summary>
|
||||
/// <param name="user"/>
|
||||
/// <returns/>
|
||||
public Task DeleteAsync(BackOfficeIdentityUser user)
|
||||
public Task<IdentityResult> DeleteAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
|
||||
var asInt = user.Id.TryConvertTo<int>();
|
||||
if (asInt == false)
|
||||
{
|
||||
throw new InvalidOperationException("The user id must be an integer to work with the Umbraco");
|
||||
}
|
||||
|
||||
var found = _userService.GetUserById(asInt.Result);
|
||||
var found = _userService.GetUserById(user.Id);
|
||||
if (found != null)
|
||||
{
|
||||
_userService.Delete(found);
|
||||
}
|
||||
_externalLoginService.DeleteUserLogins(asInt.Result);
|
||||
_externalLoginService.DeleteUserLogins(user.Id);
|
||||
|
||||
return Task.FromResult(0);
|
||||
return Task.FromResult(IdentityResult.Success);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds a user
|
||||
/// </summary>
|
||||
/// <param name="userId"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns/>
|
||||
public async Task<BackOfficeIdentityUser> FindByIdAsync(int userId)
|
||||
public async Task<BackOfficeIdentityUser> FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
var user = _userService.GetUserById(userId);
|
||||
if (user == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var user = _userService.GetUserById(UserIdToInt(userId));
|
||||
if (user == null) return null;
|
||||
|
||||
return await Task.FromResult(AssignLoginsCallback(_mapper.Map<BackOfficeIdentityUser>(user)));
|
||||
}
|
||||
@@ -183,9 +216,11 @@ namespace Umbraco.Core.Security
|
||||
/// Find a user by name
|
||||
/// </summary>
|
||||
/// <param name="userName"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns/>
|
||||
public async Task<BackOfficeIdentityUser> FindByNameAsync(string userName)
|
||||
public async Task<BackOfficeIdentityUser> FindByNameAsync(string userName, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
var user = _userService.GetByUsername(userName);
|
||||
if (user == null)
|
||||
@@ -202,9 +237,11 @@ namespace Umbraco.Core.Security
|
||||
/// Set the user password hash
|
||||
/// </summary>
|
||||
/// <param name="user"/><param name="passwordHash"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns/>
|
||||
public Task SetPasswordHashAsync(BackOfficeIdentityUser user, string passwordHash)
|
||||
public Task SetPasswordHashAsync(BackOfficeIdentityUser user, string passwordHash, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
if (passwordHash == null) throw new ArgumentNullException(nameof(passwordHash));
|
||||
@@ -212,16 +249,18 @@ namespace Umbraco.Core.Security
|
||||
|
||||
user.PasswordHash = passwordHash;
|
||||
|
||||
return Task.FromResult(0);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the user password hash
|
||||
/// </summary>
|
||||
/// <param name="user"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns/>
|
||||
public Task<string> GetPasswordHashAsync(BackOfficeIdentityUser user)
|
||||
public Task<string> GetPasswordHashAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
|
||||
@@ -232,9 +271,11 @@ namespace Umbraco.Core.Security
|
||||
/// Returns true if a user has a password set
|
||||
/// </summary>
|
||||
/// <param name="user"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns/>
|
||||
public Task<bool> HasPasswordAsync(BackOfficeIdentityUser user)
|
||||
public Task<bool> HasPasswordAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
|
||||
@@ -245,25 +286,29 @@ namespace Umbraco.Core.Security
|
||||
/// Set the user email
|
||||
/// </summary>
|
||||
/// <param name="user"/><param name="email"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns/>
|
||||
public Task SetEmailAsync(BackOfficeIdentityUser user, string email)
|
||||
public Task SetEmailAsync(BackOfficeIdentityUser user, string email, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
if (email.IsNullOrWhiteSpace()) throw new ArgumentNullException("email");
|
||||
if (email.IsNullOrWhiteSpace()) throw new ArgumentNullException(nameof(email));
|
||||
|
||||
user.Email = email;
|
||||
|
||||
return Task.FromResult(0);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the user email
|
||||
/// </summary>
|
||||
/// <param name="user"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns/>
|
||||
public Task<string> GetEmailAsync(BackOfficeIdentityUser user)
|
||||
public Task<string> GetEmailAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
|
||||
@@ -274,9 +319,11 @@ namespace Umbraco.Core.Security
|
||||
/// Returns true if the user email is confirmed
|
||||
/// </summary>
|
||||
/// <param name="user"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns/>
|
||||
public Task<bool> GetEmailConfirmedAsync(BackOfficeIdentityUser user)
|
||||
public Task<bool> GetEmailConfirmedAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
|
||||
@@ -287,21 +334,25 @@ namespace Umbraco.Core.Security
|
||||
/// Sets whether the user email is confirmed
|
||||
/// </summary>
|
||||
/// <param name="user"/><param name="confirmed"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns/>
|
||||
public Task SetEmailConfirmedAsync(BackOfficeIdentityUser user, bool confirmed)
|
||||
public Task SetEmailConfirmedAsync(BackOfficeIdentityUser user, bool confirmed, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
user.EmailConfirmed = confirmed;
|
||||
return Task.FromResult(0);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the user associated with this email
|
||||
/// </summary>
|
||||
/// <param name="email"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns/>
|
||||
public Task<BackOfficeIdentityUser> FindByEmailAsync(string email)
|
||||
public Task<BackOfficeIdentityUser> FindByEmailAsync(string email, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
var user = _userService.GetByEmail(email);
|
||||
var result = user == null
|
||||
@@ -311,13 +362,26 @@ namespace Umbraco.Core.Security
|
||||
return Task.FromResult(AssignLoginsCallback(result));
|
||||
}
|
||||
|
||||
public Task<string> GetNormalizedEmailAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken)
|
||||
{
|
||||
return GetEmailAsync(user, cancellationToken);
|
||||
}
|
||||
|
||||
public Task SetNormalizedEmailAsync(BackOfficeIdentityUser user, string normalizedEmail, CancellationToken cancellationToken)
|
||||
{
|
||||
return SetEmailAsync(user, normalizedEmail, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a user login with the specified provider and key
|
||||
/// </summary>
|
||||
/// <param name="user"/><param name="login"/>
|
||||
/// <param name="user"/>
|
||||
/// <param name="login"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns/>
|
||||
public Task AddLoginAsync(BackOfficeIdentityUser user, UserLoginInfo login)
|
||||
public Task AddLoginAsync(BackOfficeIdentityUser user, UserLoginInfo login, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
if (login == null) throw new ArgumentNullException(nameof(login));
|
||||
@@ -327,53 +391,55 @@ namespace Umbraco.Core.Security
|
||||
var userLogin = instance;
|
||||
logins.Add(userLogin);
|
||||
|
||||
return Task.FromResult(0);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the user login with the specified combination if it exists
|
||||
/// </summary>
|
||||
/// <param name="user"/><param name="login"/>
|
||||
/// <param name="user"/>
|
||||
/// <param name="providerKey"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <param name="loginProvider"></param>
|
||||
/// <returns/>
|
||||
public Task RemoveLoginAsync(BackOfficeIdentityUser user, UserLoginInfo login)
|
||||
public Task RemoveLoginAsync(BackOfficeIdentityUser user, string loginProvider, string providerKey, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
if (login == null) throw new ArgumentNullException(nameof(login));
|
||||
|
||||
var provider = login.LoginProvider;
|
||||
var key = login.ProviderKey;
|
||||
var userLogin = user.Logins.SingleOrDefault((l => l.LoginProvider == provider && l.ProviderKey == key));
|
||||
if (userLogin != null)
|
||||
user.Logins.Remove(userLogin);
|
||||
var userLogin = user.Logins.SingleOrDefault(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey);
|
||||
if (userLogin != null) user.Logins.Remove(userLogin);
|
||||
|
||||
return Task.FromResult(0);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the linked accounts for this user
|
||||
/// </summary>
|
||||
/// <param name="user"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns/>
|
||||
public Task<IList<UserLoginInfo>> GetLoginsAsync(BackOfficeIdentityUser user)
|
||||
public Task<IList<UserLoginInfo>> GetLoginsAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
return Task.FromResult((IList<UserLoginInfo>)
|
||||
user.Logins.Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey)).ToList());
|
||||
user.Logins.Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey, l.LoginProvider)).ToList());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the user associated with this login
|
||||
/// </summary>
|
||||
/// <returns/>
|
||||
public Task<BackOfficeIdentityUser> FindAsync(UserLoginInfo login)
|
||||
public Task<BackOfficeIdentityUser> FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (login == null) throw new ArgumentNullException(nameof(login));
|
||||
|
||||
//get all logins associated with the login id
|
||||
var result = _externalLoginService.Find(UserLoginInfoWrapper.Wrap(login)).ToArray();
|
||||
var result = _externalLoginService.Find(UserLoginInfoWrapper.Wrap(new UserLoginInfo(loginProvider, providerKey, loginProvider))).ToArray();
|
||||
if (result.Any())
|
||||
{
|
||||
//return the first user that matches the result
|
||||
@@ -398,54 +464,62 @@ namespace Umbraco.Core.Security
|
||||
/// <summary>
|
||||
/// Adds a user to a role (user group)
|
||||
/// </summary>
|
||||
/// <param name="user"/><param name="roleName"/>
|
||||
/// <param name="user"/>
|
||||
/// <param name="normalizedRoleName"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns/>
|
||||
public Task AddToRoleAsync(BackOfficeIdentityUser user, string roleName)
|
||||
public Task AddToRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
if (roleName == null) throw new ArgumentNullException(nameof(roleName));
|
||||
if (string.IsNullOrWhiteSpace(roleName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(roleName));
|
||||
if (normalizedRoleName == null) throw new ArgumentNullException(nameof(normalizedRoleName));
|
||||
if (string.IsNullOrWhiteSpace(normalizedRoleName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(normalizedRoleName));
|
||||
|
||||
var userRole = user.Roles.SingleOrDefault(r => r.RoleId == roleName);
|
||||
var userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName);
|
||||
|
||||
if (userRole == null)
|
||||
{
|
||||
user.AddRole(roleName);
|
||||
user.AddRole(normalizedRoleName);
|
||||
}
|
||||
|
||||
return Task.FromResult(0);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the role (user group) for the user
|
||||
/// </summary>
|
||||
/// <param name="user"/><param name="roleName"/>
|
||||
/// <param name="user"/>
|
||||
/// <param name="normalizedRoleName"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns/>
|
||||
public Task RemoveFromRoleAsync(BackOfficeIdentityUser user, string roleName)
|
||||
public Task RemoveFromRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
if (roleName == null) throw new ArgumentNullException(nameof(roleName));
|
||||
if (string.IsNullOrWhiteSpace(roleName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(roleName));
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var userRole = user.Roles.SingleOrDefault(r => r.RoleId == roleName);
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
if (normalizedRoleName == null) throw new ArgumentNullException(nameof(normalizedRoleName));
|
||||
if (string.IsNullOrWhiteSpace(normalizedRoleName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(normalizedRoleName));
|
||||
|
||||
var userRole = user.Roles.SingleOrDefault(r => r.RoleId == normalizedRoleName);
|
||||
|
||||
if (userRole != null)
|
||||
{
|
||||
user.Roles.Remove(userRole);
|
||||
}
|
||||
|
||||
return Task.FromResult(0);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the roles (user groups) for this user
|
||||
/// </summary>
|
||||
/// <param name="user"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns/>
|
||||
public Task<IList<string>> GetRolesAsync(BackOfficeIdentityUser user)
|
||||
public Task<IList<string>> GetRolesAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
return Task.FromResult((IList<string>)user.Roles.Select(x => x.RoleId).ToList());
|
||||
@@ -454,36 +528,64 @@ namespace Umbraco.Core.Security
|
||||
/// <summary>
|
||||
/// Returns true if a user is in the role
|
||||
/// </summary>
|
||||
/// <param name="user"/><param name="roleName"/>
|
||||
/// <param name="user"/>
|
||||
/// <param name="normalizedRoleName"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns/>
|
||||
public Task<bool> IsInRoleAsync(BackOfficeIdentityUser user, string roleName)
|
||||
public Task<bool> IsInRoleAsync(BackOfficeIdentityUser user, string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
return Task.FromResult(user.Roles.Select(x => x.RoleId).InvariantContains(roleName));
|
||||
return Task.FromResult(user.Roles.Select(x => x.RoleId).InvariantContains(normalizedRoleName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lists all users of a given role.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Identity Role names are equal to Umbraco UserGroup alias.
|
||||
/// </remarks>
|
||||
public Task<IList<BackOfficeIdentityUser>> GetUsersInRoleAsync(string normalizedRoleName, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (normalizedRoleName == null) throw new ArgumentNullException(nameof(normalizedRoleName));
|
||||
|
||||
var userGroup = _userService.GetUserGroupByAlias(normalizedRoleName);
|
||||
|
||||
var users = _userService.GetAllInGroup(userGroup.Id);
|
||||
IList<BackOfficeIdentityUser> backOfficeIdentityUsers = users.Select(x => _mapper.Map<BackOfficeIdentityUser>(x)).ToList();
|
||||
|
||||
return Task.FromResult(backOfficeIdentityUsers);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the security stamp for the user
|
||||
/// </summary>
|
||||
/// <param name="user"/><param name="stamp"/>
|
||||
/// <param name="user"/>
|
||||
/// <param name="stamp"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns/>
|
||||
public Task SetSecurityStampAsync(BackOfficeIdentityUser user, string stamp)
|
||||
public Task SetSecurityStampAsync(BackOfficeIdentityUser user, string stamp, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
|
||||
user.SecurityStamp = stamp;
|
||||
return Task.FromResult(0);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the user security stamp
|
||||
/// </summary>
|
||||
/// <param name="user"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns/>
|
||||
public Task<string> GetSecurityStampAsync(BackOfficeIdentityUser user)
|
||||
public Task<string> GetSecurityStampAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
|
||||
@@ -506,12 +608,17 @@ namespace Umbraco.Core.Security
|
||||
/// <summary>
|
||||
/// Sets whether two factor authentication is enabled for the user
|
||||
/// </summary>
|
||||
/// <param name="user"/><param name="enabled"/>
|
||||
/// <param name="user"/>
|
||||
/// <param name="enabled"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns/>
|
||||
public virtual Task SetTwoFactorEnabledAsync(BackOfficeIdentityUser user, bool enabled)
|
||||
public virtual Task SetTwoFactorEnabledAsync(BackOfficeIdentityUser user, bool enabled, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
|
||||
user.TwoFactorEnabled = false;
|
||||
return Task.FromResult(0);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -519,8 +626,11 @@ namespace Umbraco.Core.Security
|
||||
/// </summary>
|
||||
/// <param name="user"/>
|
||||
/// <returns/>
|
||||
public virtual Task<bool> GetTwoFactorEnabledAsync(BackOfficeIdentityUser user)
|
||||
public virtual Task<bool> GetTwoFactorEnabledAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
@@ -530,42 +640,53 @@ namespace Umbraco.Core.Security
|
||||
/// Returns the DateTimeOffset that represents the end of a user's lockout, any time in the past should be considered not locked out.
|
||||
/// </summary>
|
||||
/// <param name="user"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns/>
|
||||
/// <remarks>
|
||||
/// Currently we do not support a timed lock out, when they are locked out, an admin will have to reset the status
|
||||
/// </remarks>
|
||||
public Task<DateTimeOffset> GetLockoutEndDateAsync(BackOfficeIdentityUser user)
|
||||
public Task<DateTimeOffset?> GetLockoutEndDateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
|
||||
return user.LockoutEndDateUtc.HasValue
|
||||
? Task.FromResult(DateTimeOffset.MaxValue)
|
||||
: Task.FromResult(DateTimeOffset.MinValue);
|
||||
? Task.FromResult<DateTimeOffset?>(DateTimeOffset.MaxValue)
|
||||
: Task.FromResult<DateTimeOffset?>(DateTimeOffset.MinValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Locks a user out until the specified end date (set to a past date, to unlock a user)
|
||||
/// </summary>
|
||||
/// <param name="user"/><param name="lockoutEnd"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns/>
|
||||
/// <remarks>
|
||||
/// Currently we do not support a timed lock out, when they are locked out, an admin will have to reset the status
|
||||
/// </remarks>
|
||||
public Task SetLockoutEndDateAsync(BackOfficeIdentityUser user, DateTimeOffset lockoutEnd)
|
||||
public Task SetLockoutEndDateAsync(BackOfficeIdentityUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
user.LockoutEndDateUtc = lockoutEnd.UtcDateTime;
|
||||
return Task.FromResult(0);
|
||||
|
||||
user.LockoutEndDateUtc = lockoutEnd.Value.UtcDateTime;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to record when an attempt to access the user has failed
|
||||
/// </summary>
|
||||
/// <param name="user"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns/>
|
||||
public Task<int> IncrementAccessFailedCountAsync(BackOfficeIdentityUser user)
|
||||
public Task<int> IncrementAccessFailedCountAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
|
||||
user.AccessFailedCount++;
|
||||
return Task.FromResult(user.AccessFailedCount);
|
||||
}
|
||||
@@ -574,12 +695,16 @@ namespace Umbraco.Core.Security
|
||||
/// Used to reset the access failed count, typically after the account is successfully accessed
|
||||
/// </summary>
|
||||
/// <param name="user"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns/>
|
||||
public Task ResetAccessFailedCountAsync(BackOfficeIdentityUser user)
|
||||
public Task ResetAccessFailedCountAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
|
||||
user.AccessFailedCount = 0;
|
||||
return Task.FromResult(0);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -587,9 +712,12 @@ namespace Umbraco.Core.Security
|
||||
/// verified or the account is locked out.
|
||||
/// </summary>
|
||||
/// <param name="user"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns/>
|
||||
public Task<int> GetAccessFailedCountAsync(BackOfficeIdentityUser user)
|
||||
public Task<int> GetAccessFailedCountAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
return Task.FromResult(user.AccessFailedCount);
|
||||
}
|
||||
@@ -598,9 +726,12 @@ namespace Umbraco.Core.Security
|
||||
/// Returns true
|
||||
/// </summary>
|
||||
/// <param name="user"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns/>
|
||||
public Task<bool> GetLockoutEnabledAsync(BackOfficeIdentityUser user)
|
||||
public Task<bool> GetLockoutEnabledAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
return Task.FromResult(user.LockoutEnabled);
|
||||
}
|
||||
@@ -609,12 +740,16 @@ namespace Umbraco.Core.Security
|
||||
/// Doesn't actually perform any function, users can always be locked out
|
||||
/// </summary>
|
||||
/// <param name="user"/><param name="enabled"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns/>
|
||||
public Task SetLockoutEnabledAsync(BackOfficeIdentityUser user, bool enabled)
|
||||
public Task SetLockoutEnabledAsync(BackOfficeIdentityUser user, bool enabled, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
|
||||
user.LockoutEnabled = enabled;
|
||||
return Task.FromResult(0);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -756,21 +891,28 @@ namespace Umbraco.Core.Security
|
||||
return anythingChanged;
|
||||
}
|
||||
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(GetType().Name);
|
||||
if (_disposed) throw new ObjectDisposedException(GetType().Name);
|
||||
}
|
||||
|
||||
public Task<bool> ValidateSessionIdAsync(int userId, string sessionId)
|
||||
public Task<bool> ValidateSessionIdAsync(string userId, string sessionId)
|
||||
{
|
||||
Guid guidSessionId;
|
||||
if (Guid.TryParse(sessionId, out guidSessionId))
|
||||
{
|
||||
return Task.FromResult(_userService.ValidateLoginSession(userId, guidSessionId));
|
||||
return Task.FromResult(_userService.ValidateLoginSession(UserIdToInt(userId), guidSessionId));
|
||||
}
|
||||
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
private static int UserIdToInt(string userId)
|
||||
{
|
||||
var attempt = userId.TryConvertTo<int>();
|
||||
if (attempt.Success) return attempt.Result;
|
||||
|
||||
throw new InvalidOperationException("Unable to convert user ID to int", attempt.Exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,18 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Identity;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Umbraco.Web.Models.Identity;
|
||||
|
||||
namespace Umbraco.Core.Security
|
||||
namespace Umbraco.Web.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom validator to not validate a user's username or email if they haven't changed
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
internal class BackOfficeUserValidator<T> : UserValidator<T, int>
|
||||
public class BackOfficeUserValidator<T> : UserValidator<T>
|
||||
where T : BackOfficeIdentityUser
|
||||
{
|
||||
public BackOfficeUserValidator(UserManager<T, int> manager) : base(manager)
|
||||
public override async Task<IdentityResult> ValidateAsync(UserManager<T> manager, T user)
|
||||
{
|
||||
}
|
||||
|
||||
public override async Task<IdentityResult> ValidateAsync(T item)
|
||||
{
|
||||
//Don't validate if the user's email or username hasn't changed otherwise it's just wasting SQL queries.
|
||||
if (item.IsPropertyDirty("Email") || item.IsPropertyDirty("UserName"))
|
||||
// Don't validate if the user's email or username hasn't changed otherwise it's just wasting SQL queries.
|
||||
if (user.IsPropertyDirty("Email") || user.IsPropertyDirty("UserName"))
|
||||
{
|
||||
return await base.ValidateAsync(item);
|
||||
return await base.ValidateAsync(manager, user);
|
||||
}
|
||||
return IdentityResult.Success;
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
using System.Net.Mail;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Identity;
|
||||
|
||||
namespace Umbraco.Core.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IIdentityMessageService"/> implementation for Umbraco
|
||||
/// </summary>
|
||||
public class EmailService : IIdentityMessageService
|
||||
{
|
||||
private readonly string _notificationEmailAddress;
|
||||
private readonly IEmailSender _defaultEmailSender;
|
||||
|
||||
public EmailService(string notificationEmailAddress, IEmailSender defaultEmailSender)
|
||||
{
|
||||
_notificationEmailAddress = notificationEmailAddress;
|
||||
_defaultEmailSender = defaultEmailSender;
|
||||
}
|
||||
|
||||
|
||||
public async Task SendAsync(IdentityMessage message)
|
||||
{
|
||||
var mailMessage = new MailMessage(
|
||||
_notificationEmailAddress,
|
||||
message.Destination,
|
||||
message.Subject,
|
||||
message.Body)
|
||||
{
|
||||
IsBodyHtml = message.Body.IsNullOrWhiteSpace() == false
|
||||
&& message.Body.Contains("<") && message.Body.Contains("</")
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
//check if it's a custom message and if so use it's own defined mail sender
|
||||
var umbMsg = message as UmbracoEmailMessage;
|
||||
if (umbMsg != null)
|
||||
{
|
||||
await umbMsg.MailSender.SendAsync(mailMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _defaultEmailSender.SendAsync(mailMessage);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
mailMessage.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
using System;
|
||||
using Microsoft.AspNet.Identity.Owin;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Web.Composing;
|
||||
using Umbraco.Core.Models.Identity;
|
||||
using Umbraco.Web.Models.Identity;
|
||||
|
||||
namespace Umbraco.Web.Security
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Microsoft.Owin;
|
||||
using Umbraco.Core.Models.Identity;
|
||||
using Umbraco.Web.Models.Identity;
|
||||
|
||||
namespace Umbraco.Web.Security
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.AspNet.Identity;
|
||||
|
||||
namespace Umbraco.Core.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// A password hasher that is User aware so that it can process the hashing based on the user's settings
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey"></typeparam>
|
||||
/// <typeparam name="TUser"></typeparam>
|
||||
public interface IUserAwarePasswordHasher<in TUser, TKey> : Microsoft.AspNet.Identity.IPasswordHasher
|
||||
where TUser : class, IUser<TKey>
|
||||
where TKey : IEquatable<TKey>
|
||||
{
|
||||
string HashPassword(TUser user, string password);
|
||||
PasswordVerificationResult VerifyHashedPassword(TUser user, string hashedPassword, string providedPassword);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Identity;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Umbraco.Core.Security
|
||||
{
|
||||
@@ -8,10 +7,9 @@ namespace Umbraco.Core.Security
|
||||
/// An IUserStore interface part to implement if the store supports validating user session Ids
|
||||
/// </summary>
|
||||
/// <typeparam name="TUser"></typeparam>
|
||||
/// <typeparam name="TKey"></typeparam>
|
||||
public interface IUserSessionStore<TUser, in TKey> : IUserStore<TUser, TKey>, IDisposable
|
||||
where TUser : class, IUser<TKey>
|
||||
public interface IUserSessionStore<TUser> : IUserStore<TUser>
|
||||
where TUser : class
|
||||
{
|
||||
Task<bool> ValidateSessionIdAsync(int userId, string sessionId);
|
||||
Task<bool> ValidateSessionIdAsync(string userId, string sessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Owin;
|
||||
using Microsoft.Owin.Security.DataProtection;
|
||||
|
||||
namespace Umbraco.Web.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// Adapted from Microsoft.AspNet.Identity.Owin.IdentityFactoryMiddleware
|
||||
/// </summary>
|
||||
public class IdentityFactoryMiddleware<TResult, TOptions> : OwinMiddleware
|
||||
where TResult : class, IDisposable
|
||||
where TOptions : IdentityFactoryOptions<TResult>
|
||||
{
|
||||
/// <param name="next">The next middleware in the OWIN pipeline to invoke</param>
|
||||
/// <param name="options">Configuration options for the middleware</param>
|
||||
public IdentityFactoryMiddleware(OwinMiddleware next, TOptions options)
|
||||
: base(next)
|
||||
{
|
||||
if (options == null) throw new ArgumentNullException(nameof(options));
|
||||
if (options.Provider == null) throw new ArgumentException("options.Provider");
|
||||
|
||||
Options = options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options
|
||||
/// </summary>
|
||||
public TOptions Options { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create an object using the Options.Provider, storing it in the OwinContext and then disposes the object when finished
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
public override async Task Invoke(IOwinContext context)
|
||||
{
|
||||
var instance = Options.Provider.Create(Options, context);
|
||||
try
|
||||
{
|
||||
context.Set(instance);
|
||||
if (Next != null)
|
||||
{
|
||||
await Next.Invoke(context);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Options.Provider.Dispose(Options, instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class IdentityFactoryOptions<T> where T : class, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to configure the data protection provider
|
||||
/// </summary>
|
||||
public IDataProtectionProvider DataProtectionProvider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Provider used to Create and Dispose objects
|
||||
/// </summary>
|
||||
public IdentityFactoryProvider<T> Provider { get; set; }
|
||||
}
|
||||
|
||||
public class IdentityFactoryProvider<T> where T : class, IDisposable
|
||||
{
|
||||
public IdentityFactoryProvider()
|
||||
{
|
||||
OnDispose = (options, instance) => { };
|
||||
OnCreate = (options, context) => null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A delegate assigned to this property will be invoked when the related method is called
|
||||
/// </summary>
|
||||
public Func<IdentityFactoryOptions<T>, IOwinContext, T> OnCreate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A delegate assigned to this property will be invoked when the related method is called
|
||||
/// </summary>
|
||||
public Action<IdentityFactoryOptions<T>, T> OnDispose { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Calls the OnCreate Delegate
|
||||
/// </summary>
|
||||
/// <param name="options"></param>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
public virtual T Create(IdentityFactoryOptions<T> options, IOwinContext context)
|
||||
{
|
||||
return OnCreate(options, context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls the OnDispose delegate
|
||||
/// </summary>
|
||||
/// <param name="options"></param>
|
||||
/// <param name="instance"></param>
|
||||
public virtual void Dispose(IdentityFactoryOptions<T> options, T instance)
|
||||
{
|
||||
OnDispose(options, instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Umbraco.Web.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// No-op lookup normalizer to maintain compatibility with ASP.NET Identity 2
|
||||
/// </summary>
|
||||
public class NopLookupNormalizer : ILookupNormalizer
|
||||
{
|
||||
public string NormalizeName(string name) => name;
|
||||
|
||||
public string NormalizeEmail(string email) => email;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Owin.Security.DataProtection;
|
||||
using Umbraco.Web.Models.Identity;
|
||||
|
||||
namespace Umbraco.Web.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// Adapted from Microsoft.AspNet.Identity.Owin.DataProtectorTokenProvider
|
||||
/// </summary>
|
||||
public class OwinDataProtectorTokenProvider<TUser> : IUserTwoFactorTokenProvider<TUser> where TUser : BackOfficeIdentityUser
|
||||
{
|
||||
public TimeSpan TokenLifespan { get; set; }
|
||||
private static readonly Encoding _defaultEncoding = new UTF8Encoding(false, true);
|
||||
private readonly IDataProtector _protector;
|
||||
|
||||
public OwinDataProtectorTokenProvider(IDataProtector protector)
|
||||
{
|
||||
_protector = protector ?? throw new ArgumentNullException(nameof(protector));
|
||||
TokenLifespan = TimeSpan.FromDays(1);
|
||||
}
|
||||
|
||||
public async Task<string> GenerateAsync(string purpose, UserManager<TUser> manager, TUser user)
|
||||
{
|
||||
if (manager == null) throw new ArgumentNullException(nameof(manager));
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
|
||||
var ms = new MemoryStream();
|
||||
using (var writer = new BinaryWriter(ms, _defaultEncoding, true))
|
||||
{
|
||||
writer.Write(DateTimeOffset.UtcNow.UtcTicks);
|
||||
writer.Write(Convert.ToString(user.Id, CultureInfo.InvariantCulture));
|
||||
writer.Write(purpose ?? "");
|
||||
|
||||
string stamp = null;
|
||||
if (manager.SupportsUserSecurityStamp)
|
||||
{
|
||||
stamp = await manager.GetSecurityStampAsync(user);
|
||||
}
|
||||
writer.Write(stamp ?? "");
|
||||
}
|
||||
|
||||
var protectedBytes = _protector.Protect(ms.ToArray());
|
||||
return Convert.ToBase64String(protectedBytes);
|
||||
}
|
||||
|
||||
public async Task<bool> ValidateAsync(string purpose, string token, UserManager<TUser> manager, TUser user)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(token)) throw new ArgumentNullException(nameof(token));
|
||||
if (manager == null) throw new ArgumentNullException(nameof(manager));
|
||||
if (user == null) throw new ArgumentNullException(nameof(user));
|
||||
|
||||
try
|
||||
{
|
||||
var unprotectedData = _protector.Unprotect(Convert.FromBase64String(token));
|
||||
var ms = new MemoryStream(unprotectedData);
|
||||
using (var reader = new BinaryReader(ms, _defaultEncoding, true))
|
||||
{
|
||||
var creationTime = new DateTimeOffset(reader.ReadInt64(), TimeSpan.Zero);
|
||||
var expirationTime = creationTime + TokenLifespan;
|
||||
if (expirationTime < DateTimeOffset.UtcNow)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var userId = reader.ReadString();
|
||||
if (!string.Equals(userId, Convert.ToString(user.Id, CultureInfo.InvariantCulture)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var purp = reader.ReadString();
|
||||
if (!string.Equals(purp, purpose))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var stamp = reader.ReadString();
|
||||
if (reader.PeekChar() != -1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (manager.SupportsUserSecurityStamp)
|
||||
{
|
||||
var expectedStamp = await manager.GetSecurityStampAsync(user);
|
||||
return stamp == expectedStamp;
|
||||
}
|
||||
|
||||
return stamp == "";
|
||||
}
|
||||
}
|
||||
// ReSharper disable once EmptyGeneralCatchClause
|
||||
catch
|
||||
{
|
||||
// Do not leak exception
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<TUser> manager, TUser user)
|
||||
{
|
||||
// This token provider is designed for flows such as password reset and account confirmation
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,6 @@ using System;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using Microsoft.AspNet.Identity;
|
||||
using Microsoft.AspNet.Identity.Owin;
|
||||
using Microsoft.Owin;
|
||||
using Microsoft.Owin.Infrastructure;
|
||||
using Microsoft.Owin.Security.Cookies;
|
||||
@@ -11,7 +9,6 @@ using Umbraco.Core;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Hosting;
|
||||
using Umbraco.Core.IO;
|
||||
|
||||
using Constants = Umbraco.Core.Constants;
|
||||
|
||||
namespace Umbraco.Web.Security
|
||||
@@ -89,11 +86,11 @@ namespace Umbraco.Web.Security
|
||||
if (validate == false)
|
||||
return true;
|
||||
|
||||
var manager = owinCtx.GetUserManager<BackOfficeUserManager>();
|
||||
var manager = owinCtx.Get<BackOfficeUserManager>();
|
||||
if (manager == null)
|
||||
return false;
|
||||
|
||||
var userId = currentIdentity.GetUserId<int>();
|
||||
var userId = currentIdentity.GetUserId();
|
||||
var user = await manager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return false;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNet.Identity;
|
||||
using Umbraco.Web;
|
||||
|
||||
namespace Umbraco.Core.Security
|
||||
{
|
||||
@@ -117,7 +117,7 @@ namespace Umbraco.Core.Security
|
||||
Constants.Security.StartMediaNodeIdClaimType,
|
||||
ClaimTypes.Locality,
|
||||
Constants.Security.SessionIdClaimType,
|
||||
Microsoft.AspNet.Identity.Constants.DefaultSecurityStampClaimType
|
||||
Constants.Web.SecurityStampClaimType
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
@@ -161,8 +161,8 @@ namespace Umbraco.Core.Security
|
||||
|
||||
//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 == Microsoft.AspNet.Identity.Constants.DefaultSecurityStampClaimType) == false)
|
||||
AddClaim(new Claim(Microsoft.AspNet.Identity.Constants.DefaultSecurityStampClaimType, securityStamp, ClaimValueTypes.String, Issuer, Issuer, this));
|
||||
if (HasClaim(x => x.Type == Constants.Web.SecurityStampClaimType) == false)
|
||||
AddClaim(new Claim(Constants.Web.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)
|
||||
@@ -208,7 +208,7 @@ namespace Umbraco.Core.Security
|
||||
|
||||
public string RealName => this.FindFirstValue(ClaimTypes.GivenName);
|
||||
|
||||
public string Username => this.GetUserName();
|
||||
public string Username => this.FindFirstValue(ClaimTypes.Name);
|
||||
|
||||
public string Culture => this.FindFirstValue(ClaimTypes.Locality);
|
||||
|
||||
@@ -224,7 +224,7 @@ namespace Umbraco.Core.Security
|
||||
}
|
||||
}
|
||||
|
||||
public string SecurityStamp => this.FindFirstValue(Microsoft.AspNet.Identity.Constants.DefaultSecurityStampClaimType);
|
||||
public string SecurityStamp => this.FindFirstValue(Constants.Web.SecurityStampClaimType);
|
||||
|
||||
public string[] Roles => this.FindAll(x => x.Type == DefaultRoleClaimType).Select(role => role.Value).ToArray();
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
using Microsoft.AspNet.Identity;
|
||||
|
||||
namespace Umbraco.Core.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// A custom implementation for IdentityMessage that allows the customization of how an email is sent
|
||||
/// </summary>
|
||||
internal class UmbracoEmailMessage : IdentityMessage
|
||||
{
|
||||
public IEmailSender MailSender { get; private set; }
|
||||
|
||||
public UmbracoEmailMessage(IEmailSender mailSender)
|
||||
{
|
||||
MailSender = mailSender;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Owin.Security.Cookies;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Web.Models.Identity;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
using System;
|
||||
using Microsoft.AspNet.Identity;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Umbraco.Core.Security;
|
||||
using Umbraco.Web.Models.Identity;
|
||||
|
||||
namespace Umbraco.Core.Security
|
||||
namespace Umbraco.Web.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// The default password hasher that is User aware so that it can process the hashing based on the user's settings
|
||||
/// </summary>
|
||||
public class UserAwarePasswordHasher : IUserAwarePasswordHasher<BackOfficeIdentityUser, int>
|
||||
public class UserAwarePasswordHasher<T> : IPasswordHasher<T>
|
||||
where T : BackOfficeIdentityUser
|
||||
{
|
||||
private readonly PasswordSecurity _passwordSecurity;
|
||||
|
||||
@@ -21,27 +19,22 @@ namespace Umbraco.Core.Security
|
||||
return _passwordSecurity.HashPasswordForStorage(password);
|
||||
}
|
||||
|
||||
public string HashPassword(BackOfficeIdentityUser user, string password)
|
||||
public string HashPassword(T user, string password)
|
||||
{
|
||||
// TODO: Implement the logic for this, we need to lookup the password format for the user and hash accordingly: http://issues.umbraco.org/issue/U4-10089
|
||||
//NOTE: For now this just falls back to the hashing we are currently using
|
||||
|
||||
return HashPassword(password);
|
||||
}
|
||||
|
||||
public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
|
||||
{
|
||||
return _passwordSecurity.VerifyPassword(providedPassword, hashedPassword)
|
||||
? PasswordVerificationResult.Success
|
||||
: PasswordVerificationResult.Failed;
|
||||
}
|
||||
|
||||
public PasswordVerificationResult VerifyHashedPassword(BackOfficeIdentityUser user, string hashedPassword, string providedPassword)
|
||||
|
||||
public PasswordVerificationResult VerifyHashedPassword(T user, string hashedPassword, string providedPassword)
|
||||
{
|
||||
// TODO: Implement the logic for this, we need to lookup the password format for the user and hash accordingly: http://issues.umbraco.org/issue/U4-10089
|
||||
//NOTE: For now this just falls back to the hashing we are currently using
|
||||
|
||||
return VerifyHashedPassword(hashedPassword, providedPassword);
|
||||
return _passwordSecurity.VerifyPassword(providedPassword, hashedPassword)
|
||||
? PasswordVerificationResult.Success
|
||||
: PasswordVerificationResult.Failed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Security;
|
||||
using System.Web;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Core.Models.Membership;
|
||||
using Microsoft.AspNet.Identity.Owin;
|
||||
using Microsoft.Owin;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Hosting;
|
||||
@@ -85,7 +83,7 @@ namespace Umbraco.Web.Security
|
||||
//ensure it's done for owin too
|
||||
owinCtx.Authentication.SignOut(Constants.Security.BackOfficeExternalAuthenticationType);
|
||||
|
||||
var user = UserManager.FindByIdAsync(userId).Result;
|
||||
var user = UserManager.FindByIdAsync(userId.ToString()).Result;
|
||||
|
||||
SignInManager.SignInAsync(user, isPersistent: true, rememberBrowser: false).Wait();
|
||||
|
||||
|
||||
@@ -77,11 +77,13 @@
|
||||
</PackageReference>
|
||||
<PackageReference Include="LightInject.WebApi" Version="2.0.0" />
|
||||
<PackageReference Include="Markdown" Version="2.2.1" />
|
||||
<PackageReference Include="Microsoft.AspNet.Identity.Owin" Version="2.2.2" />
|
||||
<PackageReference Include="Microsoft.AspNet.Identity.Core" Version="2.2.3" />
|
||||
<PackageReference Include="Microsoft.AspNet.Mvc" Version="5.2.7" />
|
||||
<PackageReference Include="Microsoft.AspNet.SignalR.Core" Version="2.4.0" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi" Version="5.2.7" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="3.1.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="3.1.2" />
|
||||
<PackageReference Include="Microsoft.Owin.Host.SystemWeb" Version="4.0.1" />
|
||||
<PackageReference Include="Microsoft.Owin.Security.Cookies" Version="4.0.1" />
|
||||
<PackageReference Include="Microsoft.Owin.Security.OAuth" Version="4.0.1" />
|
||||
@@ -144,6 +146,7 @@
|
||||
<Compile Include="Compose\BackOfficeUserAuditEventsComposer.cs" />
|
||||
<Compile Include="Composing\CompositionExtensions\Installer.cs" />
|
||||
<Compile Include="Composing\LightInject\LightInjectContainer.cs" />
|
||||
<Compile Include="Security\IdentityFactoryMiddleware.cs" />
|
||||
<Compile Include="WebAssets\CDF\ClientDependencyRuntimeMinifier.cs" />
|
||||
<Compile Include="Models\NoNodesViewModel.cs" />
|
||||
<Compile Include="Mvc\RenderNoContentController.cs" />
|
||||
@@ -193,21 +196,25 @@
|
||||
<Compile Include="Profiling\WebProfilingController.cs" />
|
||||
<Compile Include="RoutableDocumentFilter.cs" />
|
||||
<Compile Include="Runtime\AspNetUmbracoBootPermissionChecker.cs" />
|
||||
<Compile Include="Security\BackOfficeClaimsPrincipalFactory.cs" />
|
||||
<Compile Include="Security\BackOfficeSignInManager.cs" />
|
||||
<Compile Include="Security\BackOfficeUserManager.cs" />
|
||||
<Compile Include="Security\BackOfficeUserManagerMarker.cs" />
|
||||
<Compile Include="WebAssets\CDF\ClientDependencyComponent.cs" />
|
||||
<Compile Include="WebAssets\CDF\ClientDependencyComposer.cs" />
|
||||
<Compile Include="Security\BackOfficeUserStore.cs" />
|
||||
<Compile Include="Security\BackOfficeUserValidator.cs" />
|
||||
<Compile Include="Security\ConfiguredPasswordValidator.cs" />
|
||||
<Compile Include="Security\EmailService.cs" />
|
||||
<Compile Include="Security\IUserAwarePasswordHasher.cs" />
|
||||
<Compile Include="Security\IUserSessionStore.cs" />
|
||||
<Compile Include="Security\MembershipProviderBase.cs" />
|
||||
<Compile Include="Security\MembershipProviderExtensions.cs" />
|
||||
<Compile Include="Security\NopLookupNormalizer.cs" />
|
||||
<Compile Include="Security\OwinDataProtectorTokenProvider.cs" />
|
||||
<Compile Include="Security\PasswordSecurity.cs" />
|
||||
<Compile Include="Security\PublicAccessChecker.cs" />
|
||||
<Compile Include="Security\UmbracoBackOfficeIdentity.cs" />
|
||||
<Compile Include="Security\UmbracoEmailMessage.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,8 +228,6 @@
|
||||
<Compile Include="WebApi\Filters\OnlyLocalRequestsAttribute.cs" />
|
||||
<Compile Include="Runtime\WebInitialComposer.cs" />
|
||||
<Compile Include="Security\ActiveDirectoryBackOfficeUserPasswordChecker.cs" />
|
||||
<Compile Include="Security\BackOfficeClaimsIdentityFactory.cs" />
|
||||
<Compile Include="Security\BackOfficeUserManagerMarker.cs" />
|
||||
<Compile Include="Security\BackOfficeUserPasswordCheckerResult.cs" />
|
||||
<Compile Include="Security\IBackOfficeUserManagerMarker.cs" />
|
||||
<Compile Include="Security\IBackOfficeUserPasswordChecker.cs" />
|
||||
@@ -238,8 +243,6 @@
|
||||
<Compile Include="Mvc\ContainerControllerFactory.cs" />
|
||||
<Compile Include="OwinExtensions.cs" />
|
||||
<Compile Include="Security\BackOfficeCookieAuthenticationProvider.cs" />
|
||||
<Compile Include="Security\BackOfficeSignInManager.cs" />
|
||||
<Compile Include="Security\BackOfficeUserManager.cs" />
|
||||
<Compile Include="Security\SessionIdValidator.cs" />
|
||||
<Compile Include="SignalR\PreviewHubComposer.cs" />
|
||||
<Compile Include="Trees\FilesTreeController.cs" />
|
||||
|
||||
@@ -90,9 +90,8 @@ namespace Umbraco.Web
|
||||
// (EXPERT: an overload accepts a custom BackOfficeUserStore implementation)
|
||||
app.ConfigureUserManagerForUmbracoBackOffice(
|
||||
Services,
|
||||
Mapper,
|
||||
ContentSettings,
|
||||
GlobalSettings,
|
||||
Mapper,
|
||||
UserPasswordConfig,
|
||||
IpResolver);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
using Umbraco.Core.Cache;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Web.WebApi.Filters;
|
||||
using Umbraco.Core.Models.Identity;
|
||||
using Umbraco.Web.WebApi.Filters;using Umbraco.Core.Models.Identity;
|
||||
using Umbraco.Core.Persistence;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Web.Models.Identity;
|
||||
|
||||
Reference in New Issue
Block a user