using System.Text.Json; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Models.Business; using Bit.Core.Models.Data.Organizations; using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Tools.Services; using Bit.Core.Vault.Repositories; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Fakes; using Bit.Test.Common.Helpers; using Fido2NetLib; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NSubstitute; using NSubstitute.ReceivedExtensions; using Xunit; namespace Bit.Core.Test.Services; [SutProviderCustomize] public class UserServiceTests { [Theory, BitAutoData] public async Task SaveUserAsync_SetsNameToNull_WhenNameIsEmpty(SutProvider sutProvider, User user) { user.Name = string.Empty; await sutProvider.Sut.SaveUserAsync(user); Assert.Null(user.Name); } [Theory, BitAutoData] public async Task UpdateLicenseAsync_Success(SutProvider sutProvider, User user, UserLicense userLicense) { using var tempDir = new TempDirectory(); var now = DateTime.UtcNow; userLicense.Issued = now.AddDays(-10); userLicense.Expires = now.AddDays(10); userLicense.Version = 1; userLicense.Premium = true; user.EmailVerified = true; user.Email = userLicense.Email; sutProvider.GetDependency().SelfHosted = true; sutProvider.GetDependency().LicenseDirectory = tempDir.Directory; sutProvider.GetDependency() .VerifyLicense(userLicense) .Returns(true); await sutProvider.Sut.UpdateLicenseAsync(user, userLicense); var filePath = Path.Combine(tempDir.Directory, "user", $"{user.Id}.json"); Assert.True(File.Exists(filePath)); var document = JsonDocument.Parse(File.OpenRead(filePath)); var root = document.RootElement; Assert.Equal(JsonValueKind.Object, root.ValueKind); // Sort of a lazy way to test that it is indented but not sure of a better way Assert.Contains('\n', root.GetRawText()); AssertHelper.AssertJsonProperty(root, "LicenseKey", JsonValueKind.String); AssertHelper.AssertJsonProperty(root, "Id", JsonValueKind.String); AssertHelper.AssertJsonProperty(root, "Premium", JsonValueKind.True); var versionProp = AssertHelper.AssertJsonProperty(root, "Version", JsonValueKind.Number); Assert.Equal(1, versionProp.GetInt32()); } [Theory, BitAutoData] public async Task SendTwoFactorEmailAsync_Success(SutProvider sutProvider, User user) { var email = user.Email.ToLowerInvariant(); var token = "thisisatokentocompare"; var userTwoFactorTokenProvider = Substitute.For>(); userTwoFactorTokenProvider .CanGenerateTwoFactorTokenAsync(Arg.Any>(), user) .Returns(Task.FromResult(true)); userTwoFactorTokenProvider .GenerateAsync("TwoFactor", Arg.Any>(), user) .Returns(Task.FromResult(token)); sutProvider.Sut.RegisterTokenProvider("Custom_Email", userTwoFactorTokenProvider); user.SetTwoFactorProviders(new Dictionary { [TwoFactorProviderType.Email] = new TwoFactorProvider { MetaData = new Dictionary { ["Email"] = email }, Enabled = true } }); await sutProvider.Sut.SendTwoFactorEmailAsync(user); await sutProvider.GetDependency() .Received(1) .SendTwoFactorEmailAsync(email, token); } [Theory, BitAutoData] public async Task SendTwoFactorEmailAsync_ExceptionBecauseNoProviderOnUser(SutProvider sutProvider, User user) { user.TwoFactorProviders = null; await Assert.ThrowsAsync("No email.", () => sutProvider.Sut.SendTwoFactorEmailAsync(user)); } [Theory, BitAutoData] public async Task SendTwoFactorEmailAsync_ExceptionBecauseNoProviderMetadataOnUser(SutProvider sutProvider, User user) { user.SetTwoFactorProviders(new Dictionary { [TwoFactorProviderType.Email] = new TwoFactorProvider { MetaData = null, Enabled = true } }); await Assert.ThrowsAsync("No email.", () => sutProvider.Sut.SendTwoFactorEmailAsync(user)); } [Theory, BitAutoData] public async Task SendTwoFactorEmailAsync_ExceptionBecauseNoProviderEmailMetadataOnUser(SutProvider sutProvider, User user) { user.SetTwoFactorProviders(new Dictionary { [TwoFactorProviderType.Email] = new TwoFactorProvider { MetaData = new Dictionary { ["qweqwe"] = user.Email.ToLowerInvariant() }, Enabled = true } }); await Assert.ThrowsAsync("No email.", () => sutProvider.Sut.SendTwoFactorEmailAsync(user)); } [Theory, BitAutoData] public async Task HasPremiumFromOrganization_Returns_False_If_No_Orgs(SutProvider sutProvider, User user) { sutProvider.GetDependency().GetManyByUserAsync(user.Id).Returns(new List()); Assert.False(await sutProvider.Sut.HasPremiumFromOrganization(user)); } [Theory] [BitAutoData(false, true)] [BitAutoData(true, false)] public async Task HasPremiumFromOrganization_Returns_False_If_Org_Not_Eligible(bool orgEnabled, bool orgUsersGetPremium, SutProvider sutProvider, User user, OrganizationUser orgUser, Organization organization) { orgUser.OrganizationId = organization.Id; organization.Enabled = orgEnabled; organization.UsersGetPremium = orgUsersGetPremium; var orgAbilities = new Dictionary() { { organization.Id, new OrganizationAbility(organization) } }; sutProvider.GetDependency().GetManyByUserAsync(user.Id).Returns(new List() { orgUser }); sutProvider.GetDependency().GetOrganizationAbilitiesAsync().Returns(orgAbilities); Assert.False(await sutProvider.Sut.HasPremiumFromOrganization(user)); } [Theory, BitAutoData] public async Task HasPremiumFromOrganization_Returns_True_If_Org_Eligible(SutProvider sutProvider, User user, OrganizationUser orgUser, Organization organization) { orgUser.OrganizationId = organization.Id; organization.Enabled = true; organization.UsersGetPremium = true; var orgAbilities = new Dictionary() { { organization.Id, new OrganizationAbility(organization) } }; sutProvider.GetDependency().GetManyByUserAsync(user.Id).Returns(new List() { orgUser }); sutProvider.GetDependency().GetOrganizationAbilitiesAsync().Returns(orgAbilities); Assert.True(await sutProvider.Sut.HasPremiumFromOrganization(user)); } [Flags] public enum ShouldCheck { Password = 0x1, OTP = 0x2, } [Theory] // A user who has a password, and the password is valid should only check for that password [BitAutoData(true, "test_password", true, ShouldCheck.Password)] // A user who does not have a password, should only check if the OTP is valid [BitAutoData(false, "otp_token", true, ShouldCheck.OTP)] // A user who has a password but supplied a OTP, it will check password first and then try OTP [BitAutoData(true, "otp_token", true, ShouldCheck.Password | ShouldCheck.OTP)] // A user who does not have a password and supplied an invalid OTP token, should only check OTP and return invalid [BitAutoData(false, "bad_otp_token", false, ShouldCheck.OTP)] // A user who does have a password but they supply a bad one, we will check both but it will still be invalid [BitAutoData(true, "bad_test_password", false, ShouldCheck.Password | ShouldCheck.OTP)] public async Task VerifySecretAsync_Works( bool shouldHavePassword, string secret, bool expectedIsVerified, ShouldCheck shouldCheck, // inline theory data SutProvider sutProvider, User user) // AutoFixture injected data { // Arrange var tokenProvider = SetupFakeTokenProvider(sutProvider, user); SetupUserAndDevice(user, shouldHavePassword); // Setup the fake password verification var substitutedUserPasswordStore = Substitute.For>(); substitutedUserPasswordStore .GetPasswordHashAsync(user, Arg.Any()) .Returns((ci) => { return Task.FromResult("hashed_test_password"); }); sutProvider.SetDependency>(substitutedUserPasswordStore, "store"); sutProvider.GetDependency>("passwordHasher") .VerifyHashedPassword(user, "hashed_test_password", "test_password") .Returns((ci) => { return PasswordVerificationResult.Success; }); // HACK: SutProvider is being weird about not injecting the IPasswordHasher that I configured var sut = new UserService( sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency>(), sutProvider.GetDependency>(), sutProvider.GetDependency>(), sutProvider.GetDependency>>(), sutProvider.GetDependency>>(), sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency>>(), sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), new FakeDataProtectorTokenFactory() ); var actualIsVerified = await sut.VerifySecretAsync(user, secret); Assert.Equal(expectedIsVerified, actualIsVerified); await tokenProvider .Received(shouldCheck.HasFlag(ShouldCheck.OTP) ? 1 : 0) .ValidateAsync(Arg.Any(), secret, Arg.Any>(), user); sutProvider.GetDependency>() .Received(shouldCheck.HasFlag(ShouldCheck.Password) ? 1 : 0) .VerifyHashedPassword(user, "hashed_test_password", secret); } [Theory, BitAutoData] public async Task IsManagedByAnyOrganizationAsync_WithManagingEnabledOrganization_ReturnsTrue( SutProvider sutProvider, Guid userId, Organization organization) { organization.Enabled = true; organization.UseSso = true; sutProvider.GetDependency() .GetByClaimedUserDomainAsync(userId) .Returns(organization); var result = await sutProvider.Sut.IsManagedByAnyOrganizationAsync(userId); Assert.True(result); } [Theory, BitAutoData] public async Task IsManagedByAnyOrganizationAsync_WithManagingDisabledOrganization_ReturnsFalse( SutProvider sutProvider, Guid userId, Organization organization) { organization.Enabled = false; organization.UseSso = true; sutProvider.GetDependency() .GetByClaimedUserDomainAsync(userId) .Returns(organization); var result = await sutProvider.Sut.IsManagedByAnyOrganizationAsync(userId); Assert.False(result); } [Theory, BitAutoData] public async Task IsManagedByAnyOrganizationAsync_WithOrganizationUseSsoFalse_ReturnsFalse( SutProvider sutProvider, Guid userId, Organization organization) { organization.Enabled = true; organization.UseSso = false; sutProvider.GetDependency() .GetByClaimedUserDomainAsync(userId) .Returns(organization); var result = await sutProvider.Sut.IsManagedByAnyOrganizationAsync(userId); Assert.False(result); } private static void SetupUserAndDevice(User user, bool shouldHavePassword) { if (shouldHavePassword) { user.MasterPassword = "test_password"; } else { user.MasterPassword = null; } } private static IUserTwoFactorTokenProvider SetupFakeTokenProvider(SutProvider sutProvider, User user) { var fakeUserTwoFactorProvider = Substitute.For>(); fakeUserTwoFactorProvider .GenerateAsync(Arg.Any(), Arg.Any>(), user) .Returns("OTP_TOKEN"); fakeUserTwoFactorProvider .ValidateAsync(Arg.Any(), Arg.Is(s => s != "otp_token"), Arg.Any>(), user) .Returns(false); fakeUserTwoFactorProvider .ValidateAsync(Arg.Any(), "otp_token", Arg.Any>(), user) .Returns(true); sutProvider.GetDependency>() .Value.Returns(new IdentityOptions { Tokens = new TokenOptions { ProviderMap = new Dictionary() { ["Email"] = new TokenProviderDescriptor(typeof(IUserTwoFactorTokenProvider)) { ProviderInstance = fakeUserTwoFactorProvider, } } } }); // The above arranging of dependencies is used in the constructor of UserManager // ref: https://github.com/dotnet/aspnetcore/blob/bfeb3bf9005c36b081d1e48725531ee0e15a9dfb/src/Identity/Extensions.Core/src/UserManager.cs#L103-L120 // since the constructor of the Sut has ran already (when injected) I need to recreate it to get it to run again sutProvider.Create(); return fakeUserTwoFactorProvider; } }