diff --git a/build/NuSpecs/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec
index 969ac5be3d..cfab130f0c 100644
--- a/build/NuSpecs/UmbracoCms.Web.nuspec
+++ b/build/NuSpecs/UmbracoCms.Web.nuspec
@@ -33,7 +33,7 @@
-
+
diff --git a/src/Umbraco.Core/ClaimsIdentityExtensions.cs b/src/Umbraco.Core/ClaimsIdentityExtensions.cs
new file mode 100644
index 0000000000..53fde97312
--- /dev/null
+++ b/src/Umbraco.Core/ClaimsIdentityExtensions.cs
@@ -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;
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs
index b1efd782fa..5602b99e2d 100644
--- a/src/Umbraco.Core/Constants-Web.cs
+++ b/src/Umbraco.Core/Constants-Web.cs
@@ -39,6 +39,16 @@
/// The route name of the page shown when Umbraco has no published content.
///
public const string NoContentRouteName = "umbraco-no-content";
+
+ ///
+ /// The claim type for the ASP.NET Identity security stamp
+ ///
+ public const string SecurityStampClaimType = "AspNet.Identity.SecurityStamp";
+
+ ///
+ /// The default authentication type used for remembering that 2FA is not needed on next login
+ ///
+ public const string TwoFactorRememberBrowserCookie = "TwoFactorRememberBrowser";
}
}
}
diff --git a/src/Umbraco.Tests/CoreThings/ClaimsIdentityExtensionsTests.cs b/src/Umbraco.Tests/CoreThings/ClaimsIdentityExtensionsTests.cs
new file mode 100644
index 0000000000..4f728c3861
--- /dev/null
+++ b/src/Umbraco.Tests/CoreThings/ClaimsIdentityExtensionsTests.cs
@@ -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(() => identity.FindFirstValue("test"));
+ }
+
+ [Test]
+ public void FindFirstValue_WhenClaimNotPresent_ExpectNull()
+ {
+ var identity = new ClaimsIdentity(new List());
+ 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 {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 {expectedClaim, dupeClaim});
+
+ var value = identity.FindFirstValue("test");
+
+ Assert.AreEqual(expectedClaim.Value, value);
+ }
+ }
+}
diff --git a/src/Umbraco.Tests/Security/AuthenticationExtensionsTests.cs b/src/Umbraco.Tests/Security/AuthenticationExtensionsTests.cs
new file mode 100644
index 0000000000..e622458c39
--- /dev/null
+++ b/src/Umbraco.Tests/Security/AuthenticationExtensionsTests.cs
@@ -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 errors = null;
+
+ Assert.Throws(() => errors.ToErrorMessage());
+ }
+
+ [Test]
+ public void ToErrorMessage_When_Single_Error_Expect_Error_Description()
+ {
+ const string expectedError = "invalid something";
+ var errors = new List {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
+ {
+ new IdentityError {Code = "1", Description = error1},
+ new IdentityError {Code = "2", Description = error2}
+ };
+
+ var errorMessage = errors.ToErrorMessage();
+
+ Assert.AreEqual($"{error1}, {error2}", errorMessage);
+ }
+ }
+}
diff --git a/src/Umbraco.Tests/Security/BackOfficeClaimsPrincipalFactoryTests.cs b/src/Umbraco.Tests/Security/BackOfficeClaimsPrincipalFactoryTests.cs
new file mode 100644
index 0000000000..b7b516318c
--- /dev/null
+++ b/src/Umbraco.Tests/Security/BackOfficeClaimsPrincipalFactoryTests.cs
@@ -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> _mockUserManager;
+
+ [Test]
+ public void Ctor_When_UserManager_Is_Null_Expect_ArgumentNullException()
+ {
+ Assert.Throws(() => new BackOfficeClaimsPrincipalFactory(
+ null,
+ new OptionsWrapper(new IdentityOptions())));
+ }
+
+ [Test]
+ public void Ctor_When_Options_Are_Null_Expect_ArgumentNullException()
+ {
+ Assert.Throws(() => new BackOfficeClaimsPrincipalFactory(
+ new Mock>(new Mock>().Object,
+ null, null, null, null, null, null, null, null).Object,
+ null));
+ }
+
+ [Test]
+ public void Ctor_When_Options_Value_Is_Null_Expect_ArgumentNullException()
+ {
+ Assert.Throws(() => new BackOfficeClaimsPrincipalFactory(
+ new Mock>(new Mock>().Object,
+ null, null, null, null, null, null, null, null).Object,
+ new OptionsWrapper(null)));
+ }
+
+ [Test]
+ public void CreateAsync_When_User_Is_Null_Expect_ArgumentNullException()
+ {
+ var sut = CreateSut();
+
+ Assert.ThrowsAsync(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{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 {ClaimType = expectedClaimType, ClaimValue = expectedClaimValue});
+ _mockUserManager.Setup(x => x.SupportsUserClaim).Returns(true);
+ _mockUserManager.Setup(x => x.GetClaimsAsync(_testUser)).ReturnsAsync(
+ new List {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();
+ mockGlobalSettings.Setup(x => x.DefaultUILanguage).Returns("test");
+
+ _testUser = new BackOfficeIdentityUser(mockGlobalSettings.Object, 2, new List())
+ {
+ UserName = "bob",
+ Name = "Bob",
+ Email = "bob@umbraco.test",
+ SecurityStamp = "B6937738-9C17-4C7D-A25A-628A875F5177"
+ };
+
+ _mockUserManager = new Mock>(new Mock>().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 CreateSut()
+ {
+ return new BackOfficeClaimsPrincipalFactory(_mockUserManager.Object,
+ new OptionsWrapper(new IdentityOptions()));
+ }
+ }
+}
diff --git a/src/Umbraco.Tests/Security/BackOfficeUserManagerTests.cs b/src/Umbraco.Tests/Security/BackOfficeUserManagerTests.cs
new file mode 100644
index 0000000000..30ed101297
--- /dev/null
+++ b/src/Umbraco.Tests/Security/BackOfficeUserManagerTests.cs
@@ -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();
+ var mockIpResolver = new Mock();
+ var mockUserStore = new Mock>();
+ var mockDataProtectionProvider = new Mock();
+
+ mockDataProtectionProvider.Setup(x => x.Create(It.IsAny()))
+ .Returns(new Mock().Object);
+ mockPasswordConfiguration.Setup(x => x.HashAlgorithmType)
+ .Returns("HMACSHA256");
+
+ var userManager = BackOfficeUserManager.Create(
+ mockPasswordConfiguration.Object,
+ mockIpResolver.Object,
+ mockUserStore.Object,
+ null,
+ mockDataProtectionProvider.Object,
+ new NullLogger>());
+
+ var mockGlobalSettings = new Mock();
+ mockGlobalSettings.Setup(x => x.DefaultUILanguage).Returns("test");
+
+ var user = new BackOfficeIdentityUser(mockGlobalSettings.Object, 2, new List())
+ {
+ UserName = "alice",
+ Name = "Alice",
+ Email = "alice@umbraco.test",
+ PasswordHash = v7Hash
+ };
+
+ mockUserStore.Setup(x => x.GetPasswordHashAsync(user, It.IsAny()))
+ .ReturnsAsync(v7Hash);
+
+ var isValidPassword = await userManager.CheckPasswordAsync(user, plaintext);
+
+ Assert.True(isValidPassword);
+ }
+ }
+}
diff --git a/src/Umbraco.Tests/Security/NopLookupNormalizerTests.cs b/src/Umbraco.Tests/Security/NopLookupNormalizerTests.cs
new file mode 100644
index 0000000000..2abecbb4dd
--- /dev/null
+++ b/src/Umbraco.Tests/Security/NopLookupNormalizerTests.cs
@@ -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);
+ }
+ }
+}
diff --git a/src/Umbraco.Tests/Security/OwinDataProtectorTokenProviderTests.cs b/src/Umbraco.Tests/Security/OwinDataProtectorTokenProviderTests.cs
new file mode 100644
index 0000000000..5e9b731aa9
--- /dev/null
+++ b/src/Umbraco.Tests/Security/OwinDataProtectorTokenProviderTests.cs
@@ -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 _mockDataProtector;
+ private Mock> _mockUserManager;
+ private BackOfficeIdentityUser _testUser;
+ private const string _testPurpose = "test";
+
+ [Test]
+ public void Ctor_When_Protector_Is_Null_Expect_ArgumentNullException()
+ {
+ Assert.Throws(() => new OwinDataProtectorTokenProvider(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(async () => await sut.GenerateAsync(null, null, _testUser));
+ }
+
+ [Test]
+ public void GenerateAsync_When_User_Is_Null_Expect_ArgumentNullException()
+ {
+ var sut = CreateSut();
+ Assert.ThrowsAsync(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(() => sut.ValidateAsync(null, token, _mockUserManager.Object, _testUser));
+ }
+
+ [Test]
+ public void ValidateAsync_When_UserManager_Is_Null_Expect_ArgumentNullException()
+ {
+ var sut = CreateSut();
+
+ Assert.ThrowsAsync(() => sut.ValidateAsync(null, Guid.NewGuid().ToString(), null, _testUser));
+ }
+
+ [Test]
+ public void ValidateAsync_When_User_Is_Null_Expect_ArgumentNullException()
+ {
+ var sut = CreateSut();
+
+ Assert.ThrowsAsync(() => 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 CreateSut()
+ => new OwinDataProtectorTokenProvider(_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();
+ _mockDataProtector.Setup(x => x.Protect(It.IsAny())).Returns((byte[] originalBytes) => originalBytes);
+ _mockDataProtector.Setup(x => x.Unprotect(It.IsAny())).Returns((byte[] originalBytes) => originalBytes);
+
+ var mockGlobalSettings = new Mock();
+ mockGlobalSettings.Setup(x => x.DefaultUILanguage).Returns("test");
+
+ _mockUserManager = new Mock>(new Mock>().Object,
+ null, null, null, null, null, null, null, null);
+ _mockUserManager.Setup(x => x.SupportsUserSecurityStamp).Returns(false);
+
+ _testUser = new BackOfficeIdentityUser(mockGlobalSettings.Object, 2, new List())
+ {
+ UserName = "alice",
+ Name = "Alice",
+ Email = "alice@umbraco.test",
+ SecurityStamp = Guid.NewGuid().ToString()
+ };
+ }
+ }
+}
diff --git a/src/Umbraco.Tests/Security/UmbracoBackOfficeIdentityTests.cs b/src/Umbraco.Tests/Security/UmbracoBackOfficeIdentityTests.cs
index beb2c0b3dc..9c16d0c35a 100644
--- a/src/Umbraco.Tests/Security/UmbracoBackOfficeIdentityTests.cs
+++ b/src/Umbraco.Tests/Security/UmbracoBackOfficeIdentityTests.cs
@@ -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);
diff --git a/src/Umbraco.Tests/Security/UmbracoSecurityStampValidatorTests.cs b/src/Umbraco.Tests/Security/UmbracoSecurityStampValidatorTests.cs
new file mode 100644
index 0000000000..92fa032271
--- /dev/null
+++ b/src/Umbraco.Tests/Security/UmbracoSecurityStampValidatorTests.cs
@@ -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 _mockOwinContext;
+ private Mock> _mockUserManager;
+ private Mock _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(() => UmbracoSecurityStampValidator
+ .OnValidateIdentity, BackOfficeIdentityUser>(
+ TimeSpan.MaxValue, null, null));
+ }
+
+ [Test]
+ public async Task OnValidateIdentity_When_Validation_Interval_Not_Met_Expect_No_Op()
+ {
+ var func = UmbracoSecurityStampValidator
+ .OnValidateIdentity, 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, BackOfficeIdentityUser>(
+ TimeSpan.MinValue, null, identity => throw new Exception());
+
+ _mockOwinContext.Setup(x => x.Get>(It.IsAny()))
+ .Returns((BackOfficeUserManager) null);
+
+ var context = new CookieValidateIdentityContext(
+ _mockOwinContext.Object,
+ _testAuthTicket,
+ _testOptions);
+
+ Assert.ThrowsAsync(async () => await func(context));
+ }
+
+ [Test]
+ public void OnValidateIdentity_When_Time_To_Validate_But_No_SignInManager_Expect_InvalidOperationException()
+ {
+ var func = UmbracoSecurityStampValidator
+ .OnValidateIdentity, BackOfficeIdentityUser>(
+ TimeSpan.MinValue, null, identity => throw new Exception());
+
+ _mockOwinContext.Setup(x => x.Get(It.IsAny()))
+ .Returns((BackOfficeSignInManager) null);
+
+ var context = new CookieValidateIdentityContext(
+ _mockOwinContext.Object,
+ _testAuthTicket,
+ _testOptions);
+
+ Assert.ThrowsAsync(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, 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, 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, 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, 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 {new Claim("sub", "bob")});
+
+ var regenFuncCalled = false;
+ Func, BackOfficeIdentityUser, Task> regenFunc =
+ (signInManager, userManager, user) =>
+ {
+ regenFuncCalled = true;
+ return Task.FromResult(expectedIdentity);
+ };
+
+ var func = UmbracoSecurityStampValidator
+ .OnValidateIdentity, 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()))
+ .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();
+ mockGlobalSettings.Setup(x => x.DefaultUILanguage).Returns("test");
+
+ _testUser = new BackOfficeIdentityUser(mockGlobalSettings.Object, 2, new List())
+ {
+ UserName = "alice",
+ Name = "Alice",
+ Email = "alice@umbraco.test",
+ SecurityStamp = Guid.NewGuid().ToString()
+ };
+
+ _testAuthTicket = new AuthenticationTicket(
+ new ClaimsIdentity(
+ new List {new Claim("sub", "alice"), new Claim(Constants.Web.SecurityStampClaimType, _testUser.SecurityStamp)},
+ _testAuthType),
+ new AuthenticationProperties());
+ _testOptions = new CookieAuthenticationOptions { AuthenticationType = _testAuthType };
+
+ _mockUserManager = new Mock>(
+ new Mock().Object,
+ new Mock().Object,
+ new Mock>().Object,
+ null, null, null, null, null, null, null);
+ _mockUserManager.Setup(x => x.FindByIdAsync(It.IsAny())).ReturnsAsync((BackOfficeIdentityUser) null);
+ _mockUserManager.Setup(x => x.SupportsUserSecurityStamp).Returns(false);
+
+ _mockSignInManager = new Mock(
+ _mockUserManager.Object,
+ new Mock>().Object,
+ new Mock().Object,
+ new Mock().Object,
+ new Mock().Object,
+ new Mock().Object);
+
+ _mockOwinContext = new Mock();
+ _mockOwinContext.Setup(x => x.Get>(It.IsAny()))
+ .Returns(_mockUserManager.Object);
+ _mockOwinContext.Setup(x => x.Get(It.IsAny()))
+ .Returns(_mockSignInManager.Object);
+
+ _mockOwinContext.Setup(x => x.Authentication.SignOut(It.IsAny()));
+ }
+ }
+}
diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj
index 82c734dffc..5227182be6 100644
--- a/src/Umbraco.Tests/Umbraco.Tests.csproj
+++ b/src/Umbraco.Tests/Umbraco.Tests.csproj
@@ -124,6 +124,7 @@
+
@@ -145,7 +146,13 @@
+
+
+
+
+
+
diff --git a/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs b/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs
index 563d9b80f8..4c373b2bc8 100644
--- a/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs
+++ b/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.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> userServiceSetup,
+ private async Task RunFipsTest(string action, Action> userServiceSetup,
Action> 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()))
+ .ReturnsAsync((BackOfficeIdentityUser) null);
+
+ Assert.ThrowsAsync(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().Object,
+ 1,
+ new List())
+ {
+ Name = "bob"
+ };
+
+ mockUserManager.Setup(x => x.FindByIdAsync(It.IsAny()))
+ .ReturnsAsync(user);
+ mockUserManager.Setup(x => x.SetLockoutEndDateAsync(user, It.IsAny()))
+ .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;
+ 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().Object,
+ 1,
+ new List())
+ {
+ Name = "bob"
+ };
+
+ mockUserManager.Setup(x => x.FindByIdAsync(user.Id.ToString()))
+ .ReturnsAsync(user);
+ mockUserManager.Setup(x => x.SetLockoutEndDateAsync(user, It.IsAny()))
+ .ReturnsAsync(IdentityResult.Success)
+ .Verifiable();
+
+ var response = await usersController.PostUnlockUsers(new[] { user.Id });
+
+ Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
+
+ var responseContent = response.Content as ObjectContent;
+ 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().Object,
+ 1,
+ new List())
+ {
+ Name = "bob"
+ };
+ var user2 = new BackOfficeIdentityUser(
+ new Mock().Object,
+ 2,
+ new List())
+ {
+ 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()))
+ .ReturnsAsync(IdentityResult.Success)
+ .Verifiable();
+ mockUserManager.Setup(x => x.SetLockoutEndDateAsync(user2, It.IsAny()))
+ .ReturnsAsync(IdentityResult.Success)
+ .Verifiable();
+
+ var response = await usersController.PostUnlockUsers(userIdsToLock);
+
+ Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
+
+ var responseContent = response.Content as ObjectContent;
+ var notifications = responseContent?.Value as SimpleNotificationModel;
+ Assert.NotNull(notifications);
+ Assert.AreEqual(userIdsToLock.Length.ToString(), notifications.Message);
+ mockUserManager.Verify();
+ }
+
+ private UsersController CreateSut(IMock> mockUserManager = null)
+ {
+ var mockLocalizedTextService = new Mock();
+ mockLocalizedTextService.Setup(x => x.Localize(It.IsAny(), It.IsAny(), It.IsAny>()))
+ .Returns((string key, CultureInfo ci, IDictionary tokens)
+ => tokens.Aggregate("", (current, next) => current + (current == string.Empty ? "" : ",") + next.Value));
+
+ var usersController = new UsersController(
+ Factory.GetInstance(),
+ Factory.GetInstance(),
+ Factory.GetInstance(),
+ ServiceContext.CreatePartial(localizedTextService: mockLocalizedTextService.Object),
+ Factory.GetInstance(),
+ Factory.GetInstance(),
+ Factory.GetInstance(),
+ Factory.GetInstance(),
+ ShortStringHelper,
+ Factory.GetInstance(),
+ Factory.GetInstance(),
+ Factory.GetInstance(),
+ Factory.GetInstance(),
+ Factory.GetInstance(),
+ Factory.GetInstance());
+
+ var mockOwinContext = new Mock();
+ var mockUserManagerMarker = new Mock();
+
+ mockOwinContext.Setup(x => x.Get(It.IsAny()))
+ .Returns(mockUserManagerMarker.Object);
+ mockUserManagerMarker.Setup(x => x.GetManager(It.IsAny()))
+ .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> CreateMockUserManager()
+ {
+ return new Mock>(
+ new Mock().Object,
+ new Mock().Object,
+ new Mock>().Object,
+ null, null, null, null, null, null, null);
+ }
}
}
diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj
index 9d2602ad6b..5e66765379 100644
--- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj
+++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj
@@ -88,7 +88,6 @@
-
diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs
index 29a13ea6aa..fc34a35566 100644
--- a/src/Umbraco.Web/Editors/AuthenticationController.cs
+++ b/src/Umbraco.Web/Editors/AuthenticationController.cs
@@ -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 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(),
- new UserLoginInfo(unlinkLoginModel.LoginProvider, unlinkLoginModel.ProviderKey));
+ user,
+ unlinkLoginModel.LoginProvider,
+ unlinkLoginModel.ProviderKey);
if (result.Succeeded)
{
- var user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
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> 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);
}
///
@@ -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> Get2FAProviders()
{
var userId = await SignInManager.GetVerifiedUserIdAsync();
- if (userId == int.MinValue)
+ if (string.IsNullOrWhiteSpace(userId))
{
Logger.Warn("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("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");
}
///
@@ -420,25 +424,27 @@ namespace Umbraco.Web.Editors
[SetAngularAntiForgeryTokens]
public async Task 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("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("Could not unlock for user {UserId} - error {UnlockError}", model.UserId, unlockResult.Errors.First());
+ Logger.Warn("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("Could not reset access failed count {UserId} - error {UnlockError}", model.UserId, unlockResult.Errors.First());
+ Logger.Warn("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);
}
}
-
}
}
diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs
index 587e6c5ea3..7e3638d96a 100644
--- a/src/Umbraco.Web/Editors/BackOfficeController.cs
+++ b/src/Umbraco.Web/Editors/BackOfficeController.cs
@@ -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("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("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("Could not verify email, Error: {Errors}, Token: {Invite}", string.Join(",", result.Errors), invite);
+ Logger.Warn("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 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(), 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
diff --git a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs
index 058f787324..4ede63b294 100644
--- a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs
+++ b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs
@@ -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
{
diff --git a/src/Umbraco.Web/Editors/CurrentUserController.cs b/src/Umbraco.Web/Editors/CurrentUserController.cs
index 5715d0ca7c..2ad09dc895 100644
--- a/src/Umbraco.Web/Editors/CurrentUserController.cs
+++ b/src/Umbraco.Web/Editors/CurrentUserController.cs
@@ -158,15 +158,16 @@ namespace Umbraco.Web.Editors
[OverrideAuthorization]
public async Task 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));
}
diff --git a/src/Umbraco.Web/Editors/PasswordChanger.cs b/src/Umbraco.Web/Editors/PasswordChanger.cs
index 179ef8fb45..b6757f76cc 100644
--- a/src/Umbraco.Web/Editors/PasswordChanger.cs
+++ b/src/Umbraco.Web/Editors/PasswordChanger.cs
@@ -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("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("Could not change user password {PasswordErrors}", errors);
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult(errors, new[] { "password" }) });
}
diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs
index 843b7cc583..7aa71b6e2e 100644
--- a/src/Umbraco.Web/Editors/UsersController.cs
+++ b/src/Umbraco.Web/Editors/UsersController.cs
@@ -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 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")]
diff --git a/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs b/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs
index eefb02ef6d..e680f155ca 100644
--- a/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs
+++ b/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs
@@ -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;
diff --git a/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs b/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs
index d93fd3a131..fedb93d6f1 100644
--- a/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs
+++ b/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs
@@ -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)
{
diff --git a/src/Umbraco.Web/Models/Identity/IdentityUser.cs b/src/Umbraco.Web/Models/Identity/IdentityUser.cs
index df104fcafe..7bd077e879 100644
--- a/src/Umbraco.Web/Models/Identity/IdentityUser.cs
+++ b/src/Umbraco.Web/Models/Identity/IdentityUser.cs
@@ -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
///
- public class IdentityUser : IUser
+ public class IdentityUser
where TLogin : IIdentityUserLogin
//NOTE: Making our role id a string
where TRole : IdentityUserRole
diff --git a/src/Umbraco.Web/Models/Identity/UserLoginInfoWrapper.cs b/src/Umbraco.Web/Models/Identity/UserLoginInfoWrapper.cs
index cd3cd51d3f..336b4c9e72 100644
--- a/src/Umbraco.Web/Models/Identity/UserLoginInfoWrapper.cs
+++ b/src/Umbraco.Web/Models/Identity/UserLoginInfoWrapper.cs
@@ -1,4 +1,4 @@
-using Microsoft.AspNet.Identity;
+using Microsoft.AspNetCore.Identity;
using Umbraco.Core.Models.Identity;
namespace Umbraco.Web.Models.Identity
diff --git a/src/Umbraco.Web/OwinExtensions.cs b/src/Umbraco.Web/OwinExtensions.cs
index 685868a76b..52c1187707 100644
--- a/src/Umbraco.Web/OwinExtensions.cs
+++ b/src/Umbraco.Web/OwinExtensions.cs
@@ -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(typeof(HttpContextBase).FullName);
return ctx == null ? Attempt.Fail() : Attempt.Succeed(ctx);
}
-
+
///
/// Gets the back office sign in manager out of OWIN
///
@@ -83,6 +80,25 @@ namespace Umbraco.Web
return marker.GetManager(owinContext)
?? throw new NullReferenceException($"Could not resolve an instance of {typeof (BackOfficeUserManager)} from the {typeof (IOwinContext)}.");
}
- }
+ ///
+ /// Adapted from Microsoft.AspNet.Identity.Owin.OwinContextExtensions
+ ///
+ public static T Get(this IOwinContext context)
+ {
+ if (context == null) throw new ArgumentNullException(nameof(context));
+ return context.Get(GetKey(typeof(T)));
+ }
+
+ public static IOwinContext Set(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;
+ }
+ }
}
diff --git a/src/Umbraco.Web/Security/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/AppBuilderExtensions.cs
index 97cc96d81b..3f129f941d 100644
--- a/src/Umbraco.Web/Security/AppBuilderExtensions.cs
+++ b/src/Umbraco.Web/Security/AppBuilderExtensions.cs
@@ -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
///
/// Configure Default Identity User Manager for Umbraco
///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
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(
(options, owinContext) => BackOfficeUserManager.Create(
- options,
services.UserService,
services.EntityService,
services.ExternalLoginService,
- mapper,
- contentSettings,
globalSettings,
+ mapper,
passwordConfiguration,
- ipResolver));
+ ipResolver,
+ new IdentityErrorDescriber(),
+ app.GetDataProtectionProvider(),
+ new NullLogger>()));
app.SetBackOfficeUserManagerType();
//Create a sign in manager per request
- app.CreatePerOwinContext((options, context) => BackOfficeSignInManager.Create(options, context, globalSettings, app.CreateLogger()));
+ app.CreatePerOwinContext((options, context) => BackOfficeSignInManager.Create(context, globalSettings, app.CreateLogger()));
}
///
/// Configure a custom UserStore with the Identity User Manager for Umbraco
///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
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(
(options, owinContext) => BackOfficeUserManager.Create(
- options,
- customUserStore,
- contentSettings,
passwordConfiguration,
ipResolver,
- globalSettings));
+ customUserStore,
+ new IdentityErrorDescriber(),
+ app.GetDataProtectionProvider(),
+ new NullLogger>()));
app.SetBackOfficeUserManagerType();
//Create a sign in manager per request
- app.CreatePerOwinContext((options, context) => BackOfficeSignInManager.Create(options, context, globalSettings, app.CreateLogger(typeof(BackOfficeSignInManager).FullName)));
- }
-
- ///
- /// Configure a custom BackOfficeUserManager for Umbraco
- ///
- ///
- ///
- ///
- ///
- public static void ConfigureUserManagerForUmbracoBackOffice(this IAppBuilder app,
- IRuntimeState runtimeState,
- IGlobalSettings globalSettings,
- Func, IOwinContext, TManager> userManager)
- where TManager : BackOfficeUserManager
- 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(userManager);
-
- app.SetBackOfficeUserManagerType();
-
- //Create a sign in manager per request
- app.CreatePerOwinContext(
- (options, context) => BackOfficeSignInManager.Create(options, context, globalSettings, app.CreateLogger(typeof(BackOfficeSignInManager).FullName)));
+ app.CreatePerOwinContext((options, context) => BackOfficeSignInManager.Create(context, globalSettings, app.CreateLogger(typeof(BackOfficeSignInManager).FullName)));
}
///
@@ -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(
+ OnValidateIdentity = UmbracoSecurityStampValidator
+ .OnValidateIdentity(
TimeSpan.FromMinutes(30),
- (manager, user) => manager.GenerateUserIdentityAsync(user),
- identity => identity.GetUserId()),
+ (signInManager, manager, user) => signInManager.CreateUserIdentityAsync(user),
+ identity => identity.GetUserId()),
};
@@ -464,5 +419,41 @@ namespace Umbraco.Web.Security
return authOptions;
}
+ public static IAppBuilder CreatePerOwinContext(this IAppBuilder app, Func createCallback)
+ where T : class, IDisposable
+ {
+ return CreatePerOwinContext(app, (options, context) => createCallback());
+ }
+
+ public static IAppBuilder CreatePerOwinContext(this IAppBuilder app,
+ Func, 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(this IAppBuilder app,
+ Func, IOwinContext, T> createCallback,
+ Action, 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>),
+ new IdentityFactoryOptions
+ {
+ DataProtectionProvider = app.GetDataProtectionProvider(),
+ Provider = new IdentityFactoryProvider
+ {
+ OnCreate = createCallback,
+ OnDispose = disposeCallback
+ }
+ });
+ return app;
+ }
}
}
diff --git a/src/Umbraco.Web/Security/AuthenticationExtensions.cs b/src/Umbraco.Web/Security/AuthenticationExtensions.cs
index ef3140bf6b..7d76eaa7be 100644
--- a/src/Umbraco.Web/Security/AuthenticationExtensions.cs
+++ b/src/Umbraco.Web/Security/AuthenticationExtensions.cs
@@ -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
///
private static readonly ConcurrentDictionary UserCultures = new ConcurrentDictionary();
+ public static string ToErrorMessage(this IEnumerable errors)
+ {
+ if (errors == null) throw new ArgumentNullException(nameof(errors));
+ return string.Join(", ", errors.Select(x => x.Description).ToList());
+ }
+
}
}
diff --git a/src/Umbraco.Web/Security/AuthenticationManagerExtensions.cs b/src/Umbraco.Web/Security/AuthenticationManagerExtensions.cs
index 5da9c77d6b..6ce1bbc86b 100644
--- a/src/Umbraco.Web/Security/AuthenticationManagerExtensions.cs
+++ b/src/Umbraco.Web/Security/AuthenticationManagerExtensions.cs
@@ -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
///
public static async Task 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 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
+ {
+ /// Associated login data
+ public UserLoginInfo Login { get; set; }
+
+ /// Suggested user name for a user
+ public string DefaultUserName { get; set; }
+
+ /// Email claim from the external identity
+ public string Email { get; set; }
+
+ /// The external identity
+ public ClaimsIdentity ExternalIdentity { get; set; }
}
}
diff --git a/src/Umbraco.Web/Security/BackOfficeClaimsIdentityFactory.cs b/src/Umbraco.Web/Security/BackOfficeClaimsIdentityFactory.cs
deleted file mode 100644
index 487e16539b..0000000000
--- a/src/Umbraco.Web/Security/BackOfficeClaimsIdentityFactory.cs
+++ /dev/null
@@ -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 : ClaimsIdentityFactory
- where T: BackOfficeIdentityUser
- {
- public BackOfficeClaimsIdentityFactory()
- {
- SecurityStampClaimType = Constants.Security.SessionIdClaimType;
- UserNameClaimType = ClaimTypes.Name;
- }
-
- ///
- /// Create a ClaimsIdentity from a user
- ///
- ///
- ///
- public override async Task CreateAsync(UserManager 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
- { }
-}
diff --git a/src/Umbraco.Web/Security/BackOfficeClaimsPrincipalFactory.cs b/src/Umbraco.Web/Security/BackOfficeClaimsPrincipalFactory.cs
new file mode 100644
index 0000000000..fe22981831
--- /dev/null
+++ b/src/Umbraco.Web/Security/BackOfficeClaimsPrincipalFactory.cs
@@ -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 : UserClaimsPrincipalFactory
+ 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 userManager, IOptions optionsAccessor)
+ : base(userManager, optionsAccessor)
+ {
+ }
+
+ public override async Task 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);
+ }
+ }
+}
diff --git a/src/Umbraco.Web/Security/BackOfficeCookieAuthenticationProvider.cs b/src/Umbraco.Web/Security/BackOfficeCookieAuthenticationProvider.cs
index 8290b8f9b7..01f8dc4e96 100644
--- a/src/Umbraco.Web/Security/BackOfficeCookieAuthenticationProvider.cs
+++ b/src/Umbraco.Web/Security/BackOfficeCookieAuthenticationProvider.cs
@@ -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);
diff --git a/src/Umbraco.Web/Security/BackOfficeSignInManager.cs b/src/Umbraco.Web/Security/BackOfficeSignInManager.cs
index 6e0ffac590..bbb4328fc3 100644
--- a/src/Umbraco.Web/Security/BackOfficeSignInManager.cs
+++ b/src/Umbraco.Web/Security/BackOfficeSignInManager.cs
@@ -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
+ ///
+ /// 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.
+ ///
+ public class BackOfficeSignInManager : IDisposable
{
+ private readonly BackOfficeUserManager _userManager;
+ private readonly IUserClaimsPrincipalFactory _claimsPrincipalFactory;
+ private readonly IAuthenticationManager _authenticationManager;
private readonly ILogger _logger;
- private readonly IOwinRequest _request;
private readonly IGlobalSettings _globalSettings;
+ private readonly IOwinRequest _request;
- public BackOfficeSignInManager(UserManager userManager, IAuthenticationManager authenticationManager, ILogger logger, IGlobalSettings globalSettings, IOwinRequest request)
- : base(userManager, authenticationManager)
+ public BackOfficeSignInManager(
+ BackOfficeUserManager userManager,
+ IUserClaimsPrincipalFactory 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 CreateUserIdentityAsync(BackOfficeIdentityUser user)
+ public async Task CreateUserIdentityAsync(BackOfficeIdentityUser user)
{
- return ((BackOfficeUserManager)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 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(userManager, new OptionsWrapper(userManager.Options)),
context.Authentication,
logger,
globalSettings,
@@ -54,30 +70,33 @@ namespace Umbraco.Web.Security
///
///
///
- public override async Task PasswordSignInAsync(string userName, string password, bool isPersistent, bool shouldLockout)
+ public async Task 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
///
///
///
- private async Task PasswordSignInAsyncImpl(string userName, string password, bool isPersistent, bool shouldLockout)
+ private async Task 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;
}
///
@@ -170,20 +182,20 @@ namespace Umbraco.Web.Security
///
///
///
- private async Task SignInOrTwoFactor(BackOfficeIdentityUser user, bool isPersistent)
+ private async Task 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;
}
///
@@ -193,12 +205,12 @@ namespace Umbraco.Web.Security
///
///
///
- 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
///
/// Replaces the underlying call which is not flexible and doesn't support a custom cookie
///
- public new async Task GetVerifiedUserIdAsync()
+ public async Task 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;
}
///
@@ -266,14 +278,14 @@ namespace Umbraco.Web.Security
///
public async Task 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;
}
-
+
///
/// Two factor verification step
///
@@ -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
///
- public override async Task TwoFactorSignInAsync(string provider, string code, bool isPersistent, bool rememberBrowser)
+ public async Task 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;
}
/// Send a two factor code to a user
@@ -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.
///
- public override async Task SendTwoFactorCodeAsync(string provider)
+ public Task 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();
}
}
}
diff --git a/src/Umbraco.Web/Security/BackOfficeUserManager.cs b/src/Umbraco.Web/Security/BackOfficeUserManager.cs
index 7de7ba37e5..a143029327 100644
--- a/src/Umbraco.Web/Security/BackOfficeUserManager.cs
+++ b/src/Umbraco.Web/Security/BackOfficeUserManager.cs
@@ -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
{
- ///
- /// Default back office user manager
- ///
public class BackOfficeUserManager : BackOfficeUserManager
{
public const string OwinMarkerKey = "Umbraco.Web.Security.Identity.BackOfficeUserManagerMarker";
public BackOfficeUserManager(
- IUserStore store,
- IdentityFactoryOptions options,
- IContentSettings contentSettingsConfig,
IPasswordConfiguration passwordConfiguration,
IIpResolver ipResolver,
- IGlobalSettings globalSettings)
- : base(store, passwordConfiguration, ipResolver)
+ IUserStore store,
+ IOptions optionsAccessor,
+ IEnumerable> userValidators,
+ IEnumerable> passwordValidators,
+ ILookupNormalizer keyNormalizer,
+ IdentityErrorDescriber errors,
+ IDataProtectionProvider dataProtectionProvider,
+ ILogger> 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
-
+
///
/// Creates a BackOfficeUserManager instance with all default options and the default BackOfficeUserManager
///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- ///
public static BackOfficeUserManager Create(
- IdentityFactoryOptions 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> 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);
}
///
/// Creates a BackOfficeUserManager instance with all default options and a custom BackOfficeUserManager instance
///
- ///
- ///
- ///
- ///
- ///
public static BackOfficeUserManager Create(
- IdentityFactoryOptions options,
- BackOfficeUserStore customUserStore,
- IContentSettings contentSettingsConfig,
IPasswordConfiguration passwordConfiguration,
IIpResolver ipResolver,
- IGlobalSettings globalSettings)
+ IUserStore customUserStore,
+ IdentityErrorDescriber errors,
+ IDataProtectionProvider dataProtectionProvider,
+ ILogger> 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> { new BackOfficeUserValidator() };
+ options.User.RequireUniqueEmail = true;
+
+ // Configure validation logic for passwords
+ var passwordValidators = new List> { new PasswordValidator() };
+ 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(options),
+ userValidators,
+ passwordValidators,
+ new NopLookupNormalizer(),
+ errors,
+ dataProtectionProvider,
+ logger);
}
+
#endregion
-
-
}
- ///
- /// Generic Back office user manager
- ///
- public class BackOfficeUserManager : UserManager
+ public class BackOfficeUserManager : UserManager
where T : BackOfficeIdentityUser
{
private PasswordGenerator _passwordGenerator;
- public BackOfficeUserManager(IUserStore store,
+ public BackOfficeUserManager(
IPasswordConfiguration passwordConfiguration,
- IIpResolver ipResolver)
- : base(store)
+ IIpResolver ipResolver,
+ IUserStore store,
+ IOptions optionsAccessor,
+ IEnumerable> userValidators,
+ IEnumerable> passwordValidators,
+ ILookupNormalizer keyNormalizer,
+ IdentityErrorDescriber errors,
+ IServiceProvider services,
+ ILogger> 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;
///
/// Developers will need to override this to support custom 2 factor auth
///
- 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 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;
- }
-
///
/// Initializes the user manager with the correct options
///
- ///
- ///
- ///
- ///
- ///
protected void InitUserManager(
BackOfficeUserManager manager,
- IPasswordConfiguration passwordConfig,
- IDataProtectionProvider dataProtectionProvider,
- IContentSettings contentSettingsConfig,
- IGlobalSettings globalSettings)
+ IDataProtectionProvider dataProtectionProvider)
{
- // Configure validation logic for usernames
- manager.UserValidator = new BackOfficeUserValidator(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(dataProtectionProvider.Create("ASP.NET Identity"))
- {
- TokenLifespan = TimeSpan.FromDays(3)
- };
+ manager.RegisterTokenProvider(
+ TokenOptions.DefaultProvider,
+ new OwinDataProtectorTokenProvider(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();
-
- 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
- //{
- // MessageFormat = "Your security code is: {0}"
- //});
- //manager.RegisterTwoFactorProvider("EmailCode", new EmailTokenProvider
- //{
- // 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());
+ manager.RegisterTokenProvider(TokenOptions.DefaultPhoneProvider, new PhoneNumberTokenProvider());
+ manager.RegisterTokenProvider(TokenOptions.DefaultAuthenticatorProvider, new AuthenticatorTokenProvider());
}
///
@@ -221,12 +190,11 @@ namespace Umbraco.Web.Security
///
///
///
- public virtual async Task ValidateSessionIdAsync(int userId, string sessionId)
+ public virtual async Task ValidateSessionIdAsync(string userId, string sessionId)
{
- var userSessionStore = Store as IUserSessionStore;
+ var userSessionStore = Store as IUserSessionStore;
//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
///
///
- protected virtual IPasswordHasher GetDefaultPasswordHasher(IPasswordConfiguration passwordConfiguration)
+ protected virtual IPasswordHasher 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(new PasswordSecurity(passwordConfiguration));
}
-
+
///
/// Gets/sets the default back office user password checker
///
@@ -262,20 +230,18 @@ namespace Umbraco.Web.Security
///
/// 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
///
- ///
+ ///
///
///
/// 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
///
- public override async Task IsLockedOutAsync(int userId)
+ public override async Task 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 ResetPasswordAsync(int userId, string token, string newPassword)
- {
- var result = base.ResetPasswordAsync(userId, token, newPassword);
- return result;
- }
-
+
///
/// This is a special method that will reset the password but will raise the Password Changed event instead of the reset event
///
@@ -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
///
- public Task ChangePasswordWithResetAsync(int userId, string token, string newPassword)
+ public async Task 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 ChangePasswordAsync(int userId, string currentPassword, string newPassword)
+ public override async Task 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;
}
-
+
///
/// Override to determine how to hash the password
///
- ///
- ///
- ///
- ///
- protected override async Task VerifyPasswordAsync(IUserPasswordStore store, T user, string password)
- {
- var userAwarePasswordHasher = PasswordHasher as IUserAwarePasswordHasher;
- if (userAwarePasswordHasher == null)
- return await base.VerifyPasswordAsync(store, user, password);
-
- var hash = await store.GetPasswordHashAsync(user);
- return userAwarePasswordHasher.VerifyHashedPassword(user, hash, password) != PasswordVerificationResult.Failed;
- }
-
- ///
- /// Override to determine how to hash the password
- ///
- ///
///
///
+ ///
///
///
/// This method is called anytime the password needs to be hashed for storage (i.e. including when reset password is used)
///
- protected override async Task UpdatePassword(IUserPasswordStore passwordStore, T user, string newPassword)
+ protected override async Task UpdatePasswordHash(T user, string newPassword, bool validatePassword)
{
user.LastPasswordChangeDateUtc = DateTime.UtcNow;
- var userAwarePasswordHasher = PasswordHasher as IUserAwarePasswordHasher;
- 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;
+ 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;
-
-
}
///
@@ -409,22 +357,20 @@ namespace Umbraco.Web.Security
///
///
///
- 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);
}
///
/// This is copied from the underlying .NET base class since they decided to not expose it
///
///
- private IUserSecurityStampStore GetSecurityStore()
+ private IUserSecurityStampStore GetSecurityStore()
{
- var store = Store as IUserSecurityStampStore;
- if (store == null)
- throw new NotSupportedException("The current user store does not implement " + typeof(IUserSecurityStampStore<>));
+ var store = Store as IUserSecurityStampStore;
+ 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 SetLockoutEndDateAsync(int userId, DateTimeOffset lockoutEnd)
+ public override async Task 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 ResetAccessFailedCountAsync(int userId)
+ public override async Task ResetAccessFailedCountAsync(T user)
{
- var lockoutStore = (IUserLockoutStore)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)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);
}
-
-
///
/// Overrides the Microsoft ASP.NET user management method
///
- ///
+ ///
///
- /// returns a Async Task
+ /// returns a Async Task
///
///
/// Doesn't set fail attempts back to 0
///
- public override async Task AccessFailedAsync(int userId)
+ public override async Task AccessFailedAsync(T user)
{
- var lockoutStore = (IUserLockoutStore)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;
+ 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);
}
-
}
-
}
diff --git a/src/Umbraco.Web/Security/BackOfficeUserManagerMarker.cs b/src/Umbraco.Web/Security/BackOfficeUserManagerMarker.cs
index 03477db730..b3eb9da3d5 100644
--- a/src/Umbraco.Web/Security/BackOfficeUserManagerMarker.cs
+++ b/src/Umbraco.Web/Security/BackOfficeUserManagerMarker.cs
@@ -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
diff --git a/src/Umbraco.Web/Security/BackOfficeUserStore.cs b/src/Umbraco.Web/Security/BackOfficeUserStore.cs
index 78eeba5eff..6f401163aa 100644
--- a/src/Umbraco.Web/Security/BackOfficeUserStore.cs
+++ b/src/Umbraco.Web/Security/BackOfficeUserStore.cs
@@ -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,
- IUserPasswordStore,
- IUserEmailStore,
- IUserLoginStore,
- IUserRoleStore,
- IUserSecurityStampStore,
- IUserLockoutStore