diff --git a/src/Core/Auth/UserFeatures/Registration/IRegisterUserCommand.cs b/src/Core/Auth/UserFeatures/Registration/IRegisterUserCommand.cs index f61cce895a..62dd9dd293 100644 --- a/src/Core/Auth/UserFeatures/Registration/IRegisterUserCommand.cs +++ b/src/Core/Auth/UserFeatures/Registration/IRegisterUserCommand.cs @@ -8,6 +8,7 @@ public interface IRegisterUserCommand /// /// Creates a new user, sends a welcome email, and raises the signup reference event. + /// This method is used for JIT of organization Users. /// /// The to create /// diff --git a/src/Identity/Controllers/AccountsController.cs b/src/Identity/Controllers/AccountsController.cs index 9360da586c..fd42074359 100644 --- a/src/Identity/Controllers/AccountsController.cs +++ b/src/Identity/Controllers/AccountsController.cs @@ -8,7 +8,6 @@ using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Services; using Bit.Core.Auth.UserFeatures.Registration; using Bit.Core.Auth.UserFeatures.WebAuthnLogin; -using Bit.Core.Auth.Utilities; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; @@ -114,16 +113,6 @@ public class AccountsController : Controller } } - [HttpPost("register")] - [CaptchaProtected] - public async Task PostRegister([FromBody] RegisterRequestModel model) - { - var user = model.ToUser(); - var identityResult = await _registerUserCommand.RegisterUserViaOrganizationInviteToken(user, model.MasterPasswordHash, - model.Token, model.OrganizationUserId); - return ProcessRegistrationResult(identityResult, user); - } - [HttpPost("register/send-verification-email")] public async Task PostRegisterSendVerificationEmail([FromBody] RegisterSendVerificationEmailRequestModel model) { @@ -175,8 +164,6 @@ public class AccountsController : Controller } return Ok(); - - } [HttpPost("register/finish")] @@ -185,7 +172,6 @@ public class AccountsController : Controller var user = model.ToUser(); // Users will either have an emailed token or an email verification token - not both. - IdentityResult identityResult = null; switch (model.GetTokenType()) @@ -196,33 +182,27 @@ public class AccountsController : Controller model.EmailVerificationToken); return ProcessRegistrationResult(identityResult, user); - break; case RegisterFinishTokenType.OrganizationInvite: identityResult = await _registerUserCommand.RegisterUserViaOrganizationInviteToken(user, model.MasterPasswordHash, model.OrgInviteToken, model.OrganizationUserId); return ProcessRegistrationResult(identityResult, user); - break; case RegisterFinishTokenType.OrgSponsoredFreeFamilyPlan: identityResult = await _registerUserCommand.RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(user, model.MasterPasswordHash, model.OrgSponsoredFreeFamilyPlanToken); return ProcessRegistrationResult(identityResult, user); - break; case RegisterFinishTokenType.EmergencyAccessInvite: Debug.Assert(model.AcceptEmergencyAccessId.HasValue); identityResult = await _registerUserCommand.RegisterUserViaAcceptEmergencyAccessInviteToken(user, model.MasterPasswordHash, model.AcceptEmergencyAccessInviteToken, model.AcceptEmergencyAccessId.Value); return ProcessRegistrationResult(identityResult, user); - break; case RegisterFinishTokenType.ProviderInvite: Debug.Assert(model.ProviderUserId.HasValue); identityResult = await _registerUserCommand.RegisterUserViaProviderInviteToken(user, model.MasterPasswordHash, model.ProviderInviteToken, model.ProviderUserId.Value); return ProcessRegistrationResult(identityResult, user); - break; - default: throw new BadRequestException("Invalid registration finish request"); } diff --git a/test/Api.IntegrationTest/Controllers/AccountsControllerTest.cs b/test/Api.IntegrationTest/Controllers/AccountsControllerTest.cs index 277f558566..4e5a6850e7 100644 --- a/test/Api.IntegrationTest/Controllers/AccountsControllerTest.cs +++ b/test/Api.IntegrationTest/Controllers/AccountsControllerTest.cs @@ -1,13 +1,6 @@ using System.Net.Http.Headers; using Bit.Api.IntegrationTest.Factories; -using Bit.Api.IntegrationTest.Helpers; using Bit.Api.Models.Response; -using Bit.Core; -using Bit.Core.Billing.Enums; -using Bit.Core.Enums; -using Bit.Core.Models.Data; -using Bit.Core.Services; -using NSubstitute; using Xunit; namespace Bit.Api.IntegrationTest.Controllers; @@ -19,7 +12,7 @@ public class AccountsControllerTest : IClassFixture public AccountsControllerTest(ApiApplicationFactory factory) => _factory = factory; [Fact] - public async Task GetPublicKey() + public async Task GetAccountsProfile_success() { var tokens = await _factory.LoginWithNewAccount(); var client = _factory.CreateClient(); @@ -33,36 +26,13 @@ public class AccountsControllerTest : IClassFixture var content = await response.Content.ReadFromJsonAsync(); Assert.NotNull(content); Assert.Equal("integration-test@bitwarden.com", content.Email); - Assert.Null(content.Name); - Assert.False(content.EmailVerified); + Assert.NotNull(content.Name); + Assert.True(content.EmailVerified); Assert.False(content.Premium); Assert.False(content.PremiumFromOrganization); Assert.Equal("en-US", content.Culture); - Assert.Null(content.Key); - Assert.Null(content.PrivateKey); + Assert.NotNull(content.Key); + Assert.NotNull(content.PrivateKey); Assert.NotNull(content.SecurityStamp); } - - private async Task SetupOrganizationManagedAccount() - { - _factory.SubstituteService(featureService => - featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true)); - - // Create the owner account - var ownerEmail = $"{Guid.NewGuid()}@bitwarden.com"; - await _factory.LoginWithNewAccount(ownerEmail); - - // Create the organization - var (_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually2023, - ownerEmail: ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card); - - // Create a new organization member - var (email, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, - OrganizationUserType.Custom, new Permissions { AccessReports = true, ManageScim = true }); - - // Add a verified domain - await OrganizationTestHelpers.CreateVerifiedDomainAsync(_factory, _organization.Id, "bitwarden.com"); - - return email; - } } diff --git a/test/Api.IntegrationTest/Factories/ApiApplicationFactory.cs b/test/Api.IntegrationTest/Factories/ApiApplicationFactory.cs index 230f0bcf08..a0963745de 100644 --- a/test/Api.IntegrationTest/Factories/ApiApplicationFactory.cs +++ b/test/Api.IntegrationTest/Factories/ApiApplicationFactory.cs @@ -1,4 +1,6 @@ -using Bit.Identity.Models.Request.Accounts; +using Bit.Core; +using Bit.Core.Auth.Models.Api.Request.Accounts; +using Bit.Core.Enums; using Bit.IntegrationTestCommon.Factories; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.TestHost; @@ -42,13 +44,23 @@ public class ApiApplicationFactory : WebApplicationFactoryBase /// /// Helper for registering and logging in to a new account /// - public async Task<(string Token, string RefreshToken)> LoginWithNewAccount(string email = "integration-test@bitwarden.com", string masterPasswordHash = "master_password_hash") + public async Task<(string Token, string RefreshToken)> LoginWithNewAccount( + string email = "integration-test@bitwarden.com", string masterPasswordHash = "master_password_hash") { - await _identityApplicationFactory.RegisterAsync(new RegisterRequestModel - { - Email = email, - MasterPasswordHash = masterPasswordHash, - }); + await _identityApplicationFactory.RegisterNewIdentityFactoryUserAsync( + new RegisterFinishRequestModel + { + Email = email, + MasterPasswordHash = masterPasswordHash, + Kdf = KdfType.PBKDF2_SHA256, + KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default, + UserAsymmetricKeys = new KeysRequestModel() + { + PublicKey = "public_key", + EncryptedPrivateKey = "private_key" + }, + UserSymmetricKey = "sym_key", + }); return await _identityApplicationFactory.TokenFromPasswordAsync(email, masterPasswordHash); } diff --git a/test/Core.Test/Auth/AutoFixture/RegisterFinishRequestModelFixtures.cs b/test/Core.Test/Auth/AutoFixture/RegisterFinishRequestModelFixtures.cs new file mode 100644 index 0000000000..a751a16f31 --- /dev/null +++ b/test/Core.Test/Auth/AutoFixture/RegisterFinishRequestModelFixtures.cs @@ -0,0 +1,58 @@ +using System.ComponentModel.DataAnnotations; +using AutoFixture; +using Bit.Core.Auth.Models.Api.Request.Accounts; +using Bit.Core.Enums; +using Bit.Core.Utilities; +using Bit.Test.Common.AutoFixture.Attributes; + +namespace Bit.Core.Test.Auth.AutoFixture; + +internal class RegisterFinishRequestModelCustomization : ICustomization +{ + [StrictEmailAddress, StringLength(256)] + public required string Email { get; set; } + public required KdfType Kdf { get; set; } + public required int KdfIterations { get; set; } + public string? EmailVerificationToken { get; set; } + public string? OrgInviteToken { get; set; } + public string? OrgSponsoredFreeFamilyPlanToken { get; set; } + public string? AcceptEmergencyAccessInviteToken { get; set; } + public string? ProviderInviteToken { get; set; } + + public void Customize(IFixture fixture) + { + fixture.Customize(composer => composer + .With(o => o.Email, Email) + .With(o => o.Kdf, Kdf) + .With(o => o.KdfIterations, KdfIterations) + .With(o => o.EmailVerificationToken, EmailVerificationToken) + .With(o => o.OrgInviteToken, OrgInviteToken) + .With(o => o.OrgSponsoredFreeFamilyPlanToken, OrgSponsoredFreeFamilyPlanToken) + .With(o => o.AcceptEmergencyAccessInviteToken, AcceptEmergencyAccessInviteToken) + .With(o => o.ProviderInviteToken, ProviderInviteToken)); + } +} + +public class RegisterFinishRequestModelCustomizeAttribute : BitCustomizeAttribute +{ + public string _email { get; set; } = "{0}@email.com"; + public KdfType _kdf { get; set; } = KdfType.PBKDF2_SHA256; + public int _kdfIterations { get; set; } = AuthConstants.PBKDF2_ITERATIONS.Default; + public string? _emailVerificationToken { get; set; } + public string? _orgInviteToken { get; set; } + public string? _orgSponsoredFreeFamilyPlanToken { get; set; } + public string? _acceptEmergencyAccessInviteToken { get; set; } + public string? _providerInviteToken { get; set; } + + public override ICustomization GetCustomization() => new RegisterFinishRequestModelCustomization() + { + Email = _email, + Kdf = _kdf, + KdfIterations = _kdfIterations, + EmailVerificationToken = _emailVerificationToken, + OrgInviteToken = _orgInviteToken, + OrgSponsoredFreeFamilyPlanToken = _orgSponsoredFreeFamilyPlanToken, + AcceptEmergencyAccessInviteToken = _acceptEmergencyAccessInviteToken, + ProviderInviteToken = _providerInviteToken + }; +} diff --git a/test/Events.IntegrationTest/EventsApplicationFactory.cs b/test/Events.IntegrationTest/EventsApplicationFactory.cs index 3faf5e81bf..b1c3ef8bf5 100644 --- a/test/Events.IntegrationTest/EventsApplicationFactory.cs +++ b/test/Events.IntegrationTest/EventsApplicationFactory.cs @@ -1,4 +1,6 @@ -using Bit.Identity.Models.Request.Accounts; +using Bit.Core; +using Bit.Core.Auth.Models.Api.Request.Accounts; +using Bit.Core.Enums; using Bit.IntegrationTestCommon.Factories; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Hosting; @@ -40,11 +42,20 @@ public class EventsApplicationFactory : WebApplicationFactoryBase /// public async Task<(string Token, string RefreshToken)> LoginWithNewAccount(string email = "integration-test@bitwarden.com", string masterPasswordHash = "master_password_hash") { - await _identityApplicationFactory.RegisterAsync(new RegisterRequestModel - { - Email = email, - MasterPasswordHash = masterPasswordHash, - }); + await _identityApplicationFactory.RegisterNewIdentityFactoryUserAsync( + new RegisterFinishRequestModel + { + Email = email, + MasterPasswordHash = masterPasswordHash, + Kdf = KdfType.PBKDF2_SHA256, + KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default, + UserAsymmetricKeys = new KeysRequestModel() + { + PublicKey = "public_key", + EncryptedPrivateKey = "private_key" + }, + UserSymmetricKey = "sym_key", + }); return await _identityApplicationFactory.TokenFromPasswordAsync(email, masterPasswordHash); } diff --git a/test/Identity.IntegrationTest/Controllers/AccountsControllerTests.cs b/test/Identity.IntegrationTest/Controllers/AccountsControllerTests.cs index 3b8534ef32..88e8af3dc6 100644 --- a/test/Identity.IntegrationTest/Controllers/AccountsControllerTests.cs +++ b/test/Identity.IntegrationTest/Controllers/AccountsControllerTests.cs @@ -8,10 +8,8 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Business.Tokenables; using Bit.Core.Repositories; -using Bit.Core.Services; using Bit.Core.Tokens; using Bit.Core.Utilities; -using Bit.Identity.Models.Request.Accounts; using Bit.IntegrationTestCommon.Factories; using Bit.Test.Common.AutoFixture.Attributes; using Microsoft.AspNetCore.DataProtection; @@ -31,24 +29,6 @@ public class AccountsControllerTests : IClassFixture _factory = factory; } - [Fact] - public async Task PostRegister_Success() - { - var context = await _factory.RegisterAsync(new RegisterRequestModel - { - Email = "test+register@email.com", - MasterPasswordHash = "master_password_hash" - }); - - Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); - - var database = _factory.GetDatabaseContext(); - var user = await database.Users - .SingleAsync(u => u.Email == "test+register@email.com"); - - Assert.NotNull(user); - } - [Theory] [BitAutoData("invalidEmail")] [BitAutoData("")] @@ -154,6 +134,7 @@ public class AccountsControllerTests : IClassFixture } [Theory, BitAutoData] + // marketing emails can stay at top level public async Task RegistrationWithEmailVerification_WithEmailVerificationToken_Succeeds([Required] string name, bool receiveMarketingEmails, [StringLength(1000), Required] string masterPasswordHash, [StringLength(50)] string masterPasswordHint, [Required] string userSymmetricKey, [Required] KeysRequestModel userAsymmetricKeys, int kdfMemory, int kdfParallelism) @@ -161,16 +142,6 @@ public class AccountsControllerTests : IClassFixture // Localize substitutions to this test. var localFactory = new IdentityApplicationFactory(); - // First we must substitute the mail service in order to be able to get a valid email verification token - // for the complete registration step - string capturedEmailVerificationToken = null; - localFactory.SubstituteService(mailService => - { - mailService.SendRegistrationVerificationEmailAsync(Arg.Any(), Arg.Do(t => capturedEmailVerificationToken = t)) - .Returns(Task.CompletedTask); - - }); - // we must first call the send verification email endpoint to trigger the first part of the process var email = $"test+register+{name}@email.com"; var sendVerificationEmailReqModel = new RegisterSendVerificationEmailRequestModel @@ -183,7 +154,7 @@ public class AccountsControllerTests : IClassFixture var sendEmailVerificationResponseHttpContext = await localFactory.PostRegisterSendEmailVerificationAsync(sendVerificationEmailReqModel); Assert.Equal(StatusCodes.Status204NoContent, sendEmailVerificationResponseHttpContext.Response.StatusCode); - Assert.NotNull(capturedEmailVerificationToken); + Assert.NotNull(localFactory.RegistrationTokens[email]); // Now we call the finish registration endpoint with the email verification token var registerFinishReqModel = new RegisterFinishRequestModel @@ -191,7 +162,7 @@ public class AccountsControllerTests : IClassFixture Email = email, MasterPasswordHash = masterPasswordHash, MasterPasswordHint = masterPasswordHint, - EmailVerificationToken = capturedEmailVerificationToken, + EmailVerificationToken = localFactory.RegistrationTokens[email], Kdf = KdfType.PBKDF2_SHA256, KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default, UserSymmetricKey = userSymmetricKey, diff --git a/test/Identity.IntegrationTest/Endpoints/IdentityServerSsoTests.cs b/test/Identity.IntegrationTest/Endpoints/IdentityServerSsoTests.cs index 602d5cfe48..c2812cc58f 100644 --- a/test/Identity.IntegrationTest/Endpoints/IdentityServerSsoTests.cs +++ b/test/Identity.IntegrationTest/Endpoints/IdentityServerSsoTests.cs @@ -1,11 +1,13 @@ using System.Security.Claims; using System.Text.Json; +using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Repositories; using Bit.Core.Entities; @@ -13,7 +15,6 @@ using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Repositories; using Bit.Core.Utilities; -using Bit.Identity.Models.Request.Accounts; using Bit.IntegrationTestCommon.Factories; using Bit.Test.Common.Helpers; using Duende.IdentityModel; @@ -545,16 +546,15 @@ public class IdentityServerSsoTests { var factory = new IdentityApplicationFactory(); - var authorizationCode = new AuthorizationCode { ClientId = "web", CreationTime = DateTime.UtcNow, Lifetime = (int)TimeSpan.FromMinutes(5).TotalSeconds, RedirectUri = "https://localhost:8080/sso-connector.html", - RequestedScopes = new[] { "api", "offline_access" }, + RequestedScopes = ["api", "offline_access"], CodeChallenge = challenge.Sha256(), - CodeChallengeMethod = "plain", // + CodeChallengeMethod = "plain", Subject = null!, // Temporarily set it to null }; @@ -564,16 +564,20 @@ public class IdentityServerSsoTests .Returns(authorizationCode); }); - // This starts the server and finalizes services - var registerResponse = await factory.RegisterAsync(new RegisterRequestModel - { - Email = TestEmail, - MasterPasswordHash = "master_password_hash", - }); - - var userRepository = factory.Services.GetRequiredService(); - var user = await userRepository.GetByEmailAsync(TestEmail); - Assert.NotNull(user); + var user = await factory.RegisterNewIdentityFactoryUserAsync( + new RegisterFinishRequestModel + { + Email = TestEmail, + MasterPasswordHash = "masterPasswordHash", + Kdf = KdfType.PBKDF2_SHA256, + KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default, + UserAsymmetricKeys = new KeysRequestModel() + { + PublicKey = "public_key", + EncryptedPrivateKey = "private_key" + }, + UserSymmetricKey = "sym_key", + }); var organizationRepository = factory.Services.GetRequiredService(); var organization = await organizationRepository.CreateAsync(new Organization diff --git a/test/Identity.IntegrationTest/Endpoints/IdentityServerTests.cs b/test/Identity.IntegrationTest/Endpoints/IdentityServerTests.cs index 38a1518d14..f4e36fa7d5 100644 --- a/test/Identity.IntegrationTest/Endpoints/IdentityServerTests.cs +++ b/test/Identity.IntegrationTest/Endpoints/IdentityServerTests.cs @@ -3,11 +3,13 @@ using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Auth.Models.Api.Request.Accounts; +using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Platform.Installations; using Bit.Core.Repositories; +using Bit.Core.Test.Auth.AutoFixture; using Bit.Identity.IdentityServer; -using Bit.Identity.Models.Request.Accounts; using Bit.IntegrationTestCommon.Factories; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; @@ -17,6 +19,7 @@ using Xunit; namespace Bit.Identity.IntegrationTest.Endpoints; +[SutProviderCustomize] public class IdentityServerTests : IClassFixture { private const int SecondsInMinute = 60; @@ -27,7 +30,7 @@ public class IdentityServerTests : IClassFixture public IdentityServerTests(IdentityApplicationFactory factory) { _factory = factory; - ReinitializeDbForTests(); + ReinitializeDbForTests(_factory); } [Fact] @@ -48,18 +51,14 @@ public class IdentityServerTests : IClassFixture AssertHelper.AssertEqualJson(endpointRoot, knownConfigurationRoot); } - [Theory, BitAutoData] - public async Task TokenEndpoint_GrantTypePassword_Success(string deviceId) + [Theory, BitAutoData, RegisterFinishRequestModelCustomize] + public async Task TokenEndpoint_GrantTypePassword_Success(RegisterFinishRequestModel requestModel) { - var username = "test+tokenpassword@email.com"; + var localFactory = new IdentityApplicationFactory(); + var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel); - await _factory.RegisterAsync(new RegisterRequestModel - { - Email = username, - MasterPasswordHash = "master_password_hash" - }); - - var context = await PostLoginAsync(_factory.Server, username, deviceId, context => context.SetAuthEmail(username)); + var context = await PostLoginAsync(localFactory.Server, user, requestModel.MasterPasswordHash, + context => context.SetAuthEmail(user.Email)); using var body = await AssertDefaultTokenBodyAsync(context); var root = body.RootElement; @@ -73,18 +72,16 @@ public class IdentityServerTests : IClassFixture AssertUserDecryptionOptions(root); } - [Theory, BitAutoData] - public async Task TokenEndpoint_GrantTypePassword_NoAuthEmailHeader_Fails(string deviceId) + [Theory, BitAutoData, RegisterFinishRequestModelCustomize] + public async Task TokenEndpoint_GrantTypePassword_NoAuthEmailHeader_Fails( + RegisterFinishRequestModel requestModel) { - var username = "test+noauthemailheader@email.com"; + requestModel.Email = "test+noauthemailheader@email.com"; - await _factory.RegisterAsync(new RegisterRequestModel - { - Email = username, - MasterPasswordHash = "master_password_hash", - }); + var localFactory = new IdentityApplicationFactory(); + var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel); - var context = await PostLoginAsync(_factory.Server, username, deviceId, null); + var context = await PostLoginAsync(localFactory.Server, user, requestModel.MasterPasswordHash, null); Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); @@ -96,18 +93,17 @@ public class IdentityServerTests : IClassFixture AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String); } - [Theory, BitAutoData] - public async Task TokenEndpoint_GrantTypePassword_InvalidBase64AuthEmailHeader_Fails(string deviceId) + [Theory, BitAutoData, RegisterFinishRequestModelCustomize] + public async Task TokenEndpoint_GrantTypePassword_InvalidBase64AuthEmailHeader_Fails( + RegisterFinishRequestModel requestModel) { - var username = "test+badauthheader@email.com"; + requestModel.Email = "test+badauthheader@email.com"; - await _factory.RegisterAsync(new RegisterRequestModel - { - Email = username, - MasterPasswordHash = "master_password_hash", - }); + var localFactory = new IdentityApplicationFactory(); + var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel); - var context = await PostLoginAsync(_factory.Server, username, deviceId, context => context.Request.Headers.Append("Auth-Email", "bad_value")); + var context = await PostLoginAsync(localFactory.Server, user, requestModel.MasterPasswordHash, + context => context.Request.Headers.Append("Auth-Email", "bad_value")); Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); @@ -119,18 +115,17 @@ public class IdentityServerTests : IClassFixture AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String); } - [Theory, BitAutoData] - public async Task TokenEndpoint_GrantTypePassword_WrongAuthEmailHeader_Fails(string deviceId) + [Theory, BitAutoData, RegisterFinishRequestModelCustomize] + public async Task TokenEndpoint_GrantTypePassword_WrongAuthEmailHeader_Fails( + RegisterFinishRequestModel requestModel) { - var username = "test+badauthheader@email.com"; + requestModel.Email = "test+badauthheader@email.com"; - await _factory.RegisterAsync(new RegisterRequestModel - { - Email = username, - MasterPasswordHash = "master_password_hash", - }); + var localFactory = new IdentityApplicationFactory(); + var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel); - var context = await PostLoginAsync(_factory.Server, username, deviceId, context => context.SetAuthEmail("bad_value")); + var context = await PostLoginAsync(localFactory.Server, user, requestModel.MasterPasswordHash, + context => context.SetAuthEmail("bad_value")); Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); @@ -142,215 +137,198 @@ public class IdentityServerTests : IClassFixture AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String); } - [Theory] + [Theory, RegisterFinishRequestModelCustomize] [BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.User)] [BitAutoData(OrganizationUserType.Custom)] - public async Task TokenEndpoint_GrantTypePassword_WithAllUserTypes_WithSsoPolicyDisabled_WithEnforceSsoPolicyForAllUsersTrue_Success(OrganizationUserType organizationUserType, Guid organizationId, string deviceId, int generatedUsername) + public async Task TokenEndpoint_GrantTypePassword_WithAllUserTypes_WithSsoPolicyDisabled_WithEnforceSsoPolicyForAllUsersTrue_Success( + OrganizationUserType organizationUserType, RegisterFinishRequestModel requestModel, Guid organizationId, int generatedUsername) { - var username = $"{generatedUsername}@example.com"; + requestModel.Email = $"{generatedUsername}@example.com"; - var server = _factory.WithWebHostBuilder(builder => + var localFactory = new IdentityApplicationFactory(); + var server = localFactory.WithWebHostBuilder(builder => { builder.UseSetting("globalSettings:sso:enforceSsoPolicyForAllUsers", "true"); }).Server; + var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel); - await server.PostAsync("/accounts/register", JsonContent.Create(new RegisterRequestModel - { - Email = username, - MasterPasswordHash = "master_password_hash" - })); + await CreateOrganizationWithSsoPolicyAsync(localFactory, + organizationId, user.Email, organizationUserType, ssoPolicyEnabled: false); - await CreateOrganizationWithSsoPolicyAsync(organizationId, username, organizationUserType, ssoPolicyEnabled: false); - - var context = await PostLoginAsync(server, username, deviceId, context => context.SetAuthEmail(username)); + var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash, + context => context.SetAuthEmail(user.Email)); Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); } - [Theory] + [Theory, RegisterFinishRequestModelCustomize] [BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.User)] [BitAutoData(OrganizationUserType.Custom)] - public async Task TokenEndpoint_GrantTypePassword_WithAllUserTypes_WithSsoPolicyDisabled_WithEnforceSsoPolicyForAllUsersFalse_Success(OrganizationUserType organizationUserType, Guid organizationId, string deviceId, int generatedUsername) + public async Task TokenEndpoint_GrantTypePassword_WithAllUserTypes_WithSsoPolicyDisabled_WithEnforceSsoPolicyForAllUsersFalse_Success( + OrganizationUserType organizationUserType, RegisterFinishRequestModel requestModel, Guid organizationId, int generatedUsername) { - var username = $"{generatedUsername}@example.com"; + requestModel.Email = $"{generatedUsername}@example.com"; - var server = _factory.WithWebHostBuilder(builder => + var localFactory = new IdentityApplicationFactory(); + var server = localFactory.WithWebHostBuilder(builder => { builder.UseSetting("globalSettings:sso:enforceSsoPolicyForAllUsers", "false"); + }).Server; + var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel); - await server.PostAsync("/accounts/register", JsonContent.Create(new RegisterRequestModel - { - Email = username, - MasterPasswordHash = "master_password_hash" - })); + await CreateOrganizationWithSsoPolicyAsync( + localFactory, organizationId, user.Email, organizationUserType, ssoPolicyEnabled: false); - await CreateOrganizationWithSsoPolicyAsync(organizationId, username, organizationUserType, ssoPolicyEnabled: false); - - var context = await PostLoginAsync(server, username, deviceId, context => context.SetAuthEmail(username)); + var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash, + context => context.SetAuthEmail(user.Email)); Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); } - [Theory] + [Theory, RegisterFinishRequestModelCustomize] [BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.User)] [BitAutoData(OrganizationUserType.Custom)] - public async Task TokenEndpoint_GrantTypePassword_WithAllUserTypes_WithSsoPolicyEnabled_WithEnforceSsoPolicyForAllUsersTrue_Throw(OrganizationUserType organizationUserType, Guid organizationId, string deviceId, int generatedUsername) + public async Task TokenEndpoint_GrantTypePassword_WithAllUserTypes_WithSsoPolicyEnabled_WithEnforceSsoPolicyForAllUsersTrue_Throw( + OrganizationUserType organizationUserType, RegisterFinishRequestModel requestModel, Guid organizationId, int generatedUsername) { - var username = $"{generatedUsername}@example.com"; + requestModel.Email = $"{generatedUsername}@example.com"; - var server = _factory.WithWebHostBuilder(builder => + var localFactory = new IdentityApplicationFactory(); + var server = localFactory.WithWebHostBuilder(builder => { builder.UseSetting("globalSettings:sso:enforceSsoPolicyForAllUsers", "true"); }).Server; + var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel); - await server.PostAsync("/accounts/register", JsonContent.Create(new RegisterRequestModel - { - Email = username, - MasterPasswordHash = "master_password_hash" - })); + await CreateOrganizationWithSsoPolicyAsync(localFactory, organizationId, user.Email, organizationUserType, ssoPolicyEnabled: true); - await CreateOrganizationWithSsoPolicyAsync(organizationId, username, organizationUserType, ssoPolicyEnabled: true); - - var context = await PostLoginAsync(server, username, deviceId, context => context.SetAuthEmail(username)); + var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash, + context => context.SetAuthEmail(user.Email)); Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); await AssertRequiredSsoAuthenticationResponseAsync(context); } - [Theory] + [Theory, RegisterFinishRequestModelCustomize] [BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Admin)] - public async Task TokenEndpoint_GrantTypePassword_WithOwnerOrAdmin_WithSsoPolicyEnabled_WithEnforceSsoPolicyForAllUsersFalse_Success(OrganizationUserType organizationUserType, Guid organizationId, string deviceId, int generatedUsername) + public async Task TokenEndpoint_GrantTypePassword_WithOwnerOrAdmin_WithSsoPolicyEnabled_WithEnforceSsoPolicyForAllUsersFalse_Success( + OrganizationUserType organizationUserType, RegisterFinishRequestModel requestModel, Guid organizationId, int generatedUsername) { - var username = $"{generatedUsername}@example.com"; + requestModel.Email = $"{generatedUsername}@example.com"; - var server = _factory.WithWebHostBuilder(builder => + var localFactory = new IdentityApplicationFactory(); + var server = localFactory.WithWebHostBuilder(builder => { builder.UseSetting("globalSettings:sso:enforceSsoPolicyForAllUsers", "false"); + }).Server; + var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel); - await server.PostAsync("/accounts/register", JsonContent.Create(new RegisterRequestModel - { - Email = username, - MasterPasswordHash = "master_password_hash" - })); + await CreateOrganizationWithSsoPolicyAsync(localFactory, organizationId, user.Email, organizationUserType, ssoPolicyEnabled: true); - await CreateOrganizationWithSsoPolicyAsync(organizationId, username, organizationUserType, ssoPolicyEnabled: true); - - var context = await PostLoginAsync(server, username, deviceId, context => context.SetAuthEmail(username)); + var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash, + context => context.SetAuthEmail(user.Email)); Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); } - [Theory] + [Theory, RegisterFinishRequestModelCustomize] [BitAutoData(OrganizationUserType.User)] [BitAutoData(OrganizationUserType.Custom)] - public async Task TokenEndpoint_GrantTypePassword_WithNonOwnerOrAdmin_WithSsoPolicyEnabled_WithEnforceSsoPolicyForAllUsersFalse_Throws(OrganizationUserType organizationUserType, Guid organizationId, string deviceId, int generatedUsername) + public async Task TokenEndpoint_GrantTypePassword_WithNonOwnerOrAdmin_WithSsoPolicyEnabled_WithEnforceSsoPolicyForAllUsersFalse_Throws( + OrganizationUserType organizationUserType, RegisterFinishRequestModel requestModel, Guid organizationId, int generatedUsername) { - var username = $"{generatedUsername}@example.com"; + requestModel.Email = $"{generatedUsername}@example.com"; - var server = _factory.WithWebHostBuilder(builder => + var localFactory = new IdentityApplicationFactory(); + var server = localFactory.WithWebHostBuilder(builder => { builder.UseSetting("globalSettings:sso:enforceSsoPolicyForAllUsers", "false"); + }).Server; + var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel); - await server.PostAsync("/accounts/register", JsonContent.Create(new RegisterRequestModel - { - Email = username, - MasterPasswordHash = "master_password_hash" - })); + await CreateOrganizationWithSsoPolicyAsync(localFactory, organizationId, user.Email, organizationUserType, ssoPolicyEnabled: true); - await CreateOrganizationWithSsoPolicyAsync(organizationId, username, organizationUserType, ssoPolicyEnabled: true); - - var context = await PostLoginAsync(server, username, deviceId, context => context.SetAuthEmail(username)); + var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash, + context => context.SetAuthEmail(user.Email)); Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); await AssertRequiredSsoAuthenticationResponseAsync(context); } - [Theory, BitAutoData] - public async Task TokenEndpoint_GrantTypeRefreshToken_Success(string deviceId) + [Theory, BitAutoData, RegisterFinishRequestModelCustomize] + public async Task TokenEndpoint_GrantTypeRefreshToken_Success(RegisterFinishRequestModel requestModel) { - var username = "test+tokenrefresh@email.com"; + var localFactory = new IdentityApplicationFactory(); - await _factory.RegisterAsync(new RegisterRequestModel - { - Email = username, - MasterPasswordHash = "master_password_hash", - }); + var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel); - var (_, refreshToken) = await _factory.TokenFromPasswordAsync(username, "master_password_hash", deviceId); + var (_, refreshToken) = await localFactory.TokenFromPasswordAsync( + requestModel.Email, requestModel.MasterPasswordHash); - var context = await _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary - { - { "grant_type", "refresh_token" }, - { "client_id", "web" }, - { "refresh_token", refreshToken }, - })); + var context = await localFactory.Server.PostAsync("/connect/token", + new FormUrlEncodedContent(new Dictionary + { + { "grant_type", "refresh_token" }, + { "client_id", "web" }, + { "refresh_token", refreshToken }, + })); using var body = await AssertDefaultTokenBodyAsync(context); AssertRefreshTokenExists(body.RootElement); } - [Theory, BitAutoData] - public async Task TokenEndpoint_GrantTypeClientCredentials_Success(string deviceId) + [Theory, BitAutoData, RegisterFinishRequestModelCustomize] + public async Task TokenEndpoint_GrantTypeClientCredentials_Success(RegisterFinishRequestModel model) { - var username = "test+tokenclientcredentials@email.com"; + var localFactory = new IdentityApplicationFactory(); + var user = await localFactory.RegisterNewIdentityFactoryUserAsync(model); - await _factory.RegisterAsync(new RegisterRequestModel - { - Email = username, - MasterPasswordHash = "master_password_hash", - }); - - var database = _factory.GetDatabaseContext(); - var user = await database.Users - .FirstAsync(u => u.Email == username); - - var context = await _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary - { - { "grant_type", "client_credentials" }, - { "client_id", $"user.{user.Id}" }, - { "client_secret", user.ApiKey }, - { "scope", "api" }, - { "DeviceIdentifier", deviceId }, - { "DeviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) }, - { "DeviceName", "firefox" }, - })); + var context = await localFactory.Server.PostAsync("/connect/token", + new FormUrlEncodedContent(new Dictionary + { + { "grant_type", "client_credentials" }, + { "client_id", $"user.{user.Id}" }, + { "client_secret", user.ApiKey }, + { "scope", "api" }, + { "DeviceIdentifier", IdentityApplicationFactory.DefaultDeviceIdentifier }, + { "DeviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) }, + { "DeviceName", "firefox" }, + }) + ); await AssertDefaultTokenBodyAsync(context, "api"); } - [Theory, BitAutoData] - public async Task TokenEndpoint_GrantTypeClientCredentials_AsLegacyUser_NotOnWebClient_Fails(string deviceId) + [Theory, BitAutoData, RegisterFinishRequestModelCustomize] + public async Task TokenEndpoint_GrantTypeClientCredentials_AsLegacyUser_NotOnWebClient_Fails( + RegisterFinishRequestModel model, + string deviceId) { - var server = _factory.WithWebHostBuilder(builder => + var localFactory = new IdentityApplicationFactory(); + var server = localFactory.WithWebHostBuilder(builder => { builder.UseSetting("globalSettings:launchDarkly:flagValues:block-legacy-users", "true"); }).Server; - var username = "test+tokenclientcredentials@email.com"; + model.Email = "test+tokenclientcredentials@email.com"; + var user = await localFactory.RegisterNewIdentityFactoryUserAsync(model); - - await server.PostAsync("/accounts/register", JsonContent.Create(new RegisterRequestModel - { - Email = username, - MasterPasswordHash = "master_password_hash" - })); - - - var database = _factory.GetDatabaseContext(); - var user = await database.Users - .FirstAsync(u => u.Email == username); - - user.PrivateKey = "EncryptedPrivateKey"; + // Modify user to be legacy user. We have to fetch the user again to put it in the ef-context + // so when we modify change tracking will save the changes. + var database = localFactory.GetDatabaseContext(); + user = await database.Users + .FirstAsync(u => u.Email == model.Email); + user.Key = null; await database.SaveChangesAsync(); var context = await server.PostAsync("/connect/token", new FormUrlEncodedContent( @@ -362,9 +340,9 @@ public class IdentityServerTests : IClassFixture { "deviceIdentifier", deviceId }, { "deviceName", "chrome" }, { "grant_type", "password" }, - { "username", username }, - { "password", "master_password_hash" }, - }), context => context.SetAuthEmail(username)); + { "username", model.Email }, + { "password", model.MasterPasswordHash }, + }), context => context.SetAuthEmail(model.Email)); Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); @@ -535,23 +513,21 @@ public class IdentityServerTests : IClassFixture Assert.Equal("invalid_client", error); } - [Theory, BitAutoData] - public async Task TokenEndpoint_TooQuickInOneSecond_BlockRequest(string deviceId) + [Theory, BitAutoData, RegisterFinishRequestModelCustomize] + public async Task TokenEndpoint_TooQuickInOneSecond_BlockRequest( + RegisterFinishRequestModel requestModel) { const int AmountInOneSecondAllowed = 10; // The rule we are testing is 10 requests in 1 second - var username = "test+ratelimiting@email.com"; + requestModel.Email = "test+ratelimiting@email.com"; - await _factory.RegisterAsync(new RegisterRequestModel - { - Email = username, - MasterPasswordHash = "master_password_hash", - }); + var localFactory = new IdentityApplicationFactory(); + var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel); - var database = _factory.GetDatabaseContext(); - var user = await database.Users - .FirstAsync(u => u.Email == username); + var database = localFactory.GetDatabaseContext(); + user = await database.Users + .FirstAsync(u => u.Email == user.Email); var tasks = new Task[AmountInOneSecondAllowed + 1]; @@ -573,36 +549,40 @@ public class IdentityServerTests : IClassFixture { "scope", "api offline_access" }, { "client_id", "web" }, { "deviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) }, - { "deviceIdentifier", deviceId }, + { "deviceIdentifier", IdentityApplicationFactory.DefaultDeviceIdentifier }, { "deviceName", "firefox" }, { "grant_type", "password" }, - { "username", username }, + { "username", user.Email}, { "password", "master_password_hash" }, - }), context => context.SetAuthEmail(username).SetIp("1.1.1.2")); + }), context => context.SetAuthEmail(user.Email).SetIp("1.1.1.2")); } } - private async Task PostLoginAsync(TestServer server, string username, string deviceId, Action extraConfiguration) + private async Task PostLoginAsync( + TestServer server, User user, string MasterPasswordHash, Action extraConfiguration) { return await server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary { { "scope", "api offline_access" }, { "client_id", "web" }, { "deviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) }, - { "deviceIdentifier", deviceId }, + { "deviceIdentifier", IdentityApplicationFactory.DefaultDeviceIdentifier }, { "deviceName", "firefox" }, { "grant_type", "password" }, - { "username", username }, - { "password", "master_password_hash" }, + { "username", user.Email }, + { "password", MasterPasswordHash }, }), extraConfiguration); } - private async Task CreateOrganizationWithSsoPolicyAsync(Guid organizationId, string username, OrganizationUserType organizationUserType, bool ssoPolicyEnabled) + private async Task CreateOrganizationWithSsoPolicyAsync( + IdentityApplicationFactory localFactory, + Guid organizationId, + string username, OrganizationUserType organizationUserType, bool ssoPolicyEnabled) { - var userRepository = _factory.Services.GetService(); - var organizationRepository = _factory.Services.GetService(); - var organizationUserRepository = _factory.Services.GetService(); - var policyRepository = _factory.Services.GetService(); + var userRepository = localFactory.Services.GetService(); + var organizationRepository = localFactory.Services.GetService(); + var organizationUserRepository = localFactory.Services.GetService(); + var policyRepository = localFactory.Services.GetService(); var organization = new Organization { @@ -617,7 +597,7 @@ public class IdentityServerTests : IClassFixture await organizationRepository.CreateAsync(organization); var user = await userRepository.GetByEmailAsync(username); - var organizationUser = new Bit.Core.Entities.OrganizationUser + var organizationUser = new OrganizationUser { OrganizationId = organization.Id, UserId = user.Id, @@ -703,9 +683,9 @@ public class IdentityServerTests : IClassFixture (prop) => { Assert.Equal("Object", prop.Name); Assert.Equal("userDecryptionOptions", prop.Value.GetString()); }); } - private void ReinitializeDbForTests() + private void ReinitializeDbForTests(IdentityApplicationFactory factory) { - var databaseContext = _factory.GetDatabaseContext(); + var databaseContext = factory.GetDatabaseContext(); databaseContext.Policies.RemoveRange(databaseContext.Policies); databaseContext.OrganizationUsers.RemoveRange(databaseContext.OrganizationUsers); databaseContext.Organizations.RemoveRange(databaseContext.Organizations); diff --git a/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs b/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs index 6f0ef20295..82c6b13aad 100644 --- a/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs +++ b/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs @@ -1,8 +1,11 @@ using System.Security.Claims; +using System.Text; using System.Text.Json; +using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Repositories; using Bit.Core.Entities; @@ -11,7 +14,6 @@ using Bit.Core.Models.Data; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Utilities; -using Bit.Identity.Models.Request.Accounts; using Bit.IntegrationTestCommon.Factories; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; @@ -19,6 +21,7 @@ using Duende.IdentityModel; using Duende.IdentityServer.Models; using Duende.IdentityServer.Stores; using LinqToDB; +using Microsoft.Extensions.Caching.Distributed; using NSubstitute; using Xunit; @@ -61,19 +64,14 @@ public class IdentityServerTwoFactorTests : IClassFixture(mailService => + // return specified email token from cache + var emailToken = "12345678"; + factory.SubstituteService(distCache => { - mailService.SendTwoFactorEmailAsync( - Arg.Any(), - Arg.Any(), - Arg.Do(t => emailToken = t), - Arg.Any(), - Arg.Any()) - .Returns(Task.CompletedTask); + distCache.GetAsync(Arg.Is(s => s.StartsWith("EmailToken_"))) + .Returns(Task.FromResult(Encoding.UTF8.GetBytes(emailToken))); }); // Create Test User @@ -102,10 +100,11 @@ public class IdentityServerTwoFactorTests : IClassFixture + var context = await localFactory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary { { "scope", "api offline_access" }, { "client_id", "web" }, @@ -156,10 +156,11 @@ public class IdentityServerTwoFactorTests : IClassFixture u.Email == _testEmail); // Act - var context = await _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary + var context = await localFactory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary { { "grant_type", "client_credentials" }, { "client_id", $"user.{user.Id}" }, @@ -275,16 +277,13 @@ public class IdentityServerTwoFactorTests : IClassFixture(mailService => + + // return specified email token from cache + var emailToken = "12345678"; + localFactory.SubstituteService(distCache => { - mailService.SendTwoFactorEmailAsync( - Arg.Any(), - Arg.Any(), - Arg.Do(t => emailToken = t), - Arg.Any(), - Arg.Any()) - .Returns(Task.CompletedTask); + distCache.GetAsync(Arg.Is(s => s.StartsWith("EmailToken_"))) + .Returns(Task.FromResult(Encoding.UTF8.GetBytes(emailToken))); }); // Create Test User @@ -379,17 +378,24 @@ public class IdentityServerTwoFactorTests : IClassFixture(); - var user = await userRepository.GetByEmailAsync(testEmail); + var user = await factory.RegisterNewIdentityFactoryUserAsync( + new RegisterFinishRequestModel + { + Email = testEmail, + MasterPasswordHash = _testPassword, + Kdf = KdfType.PBKDF2_SHA256, + KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default, + UserAsymmetricKeys = new KeysRequestModel() + { + PublicKey = "public_key", + EncryptedPrivateKey = "private_key" + }, + UserSymmetricKey = "sym_key", + }); Assert.NotNull(user); var userService = factory.GetService(); + var userRepository = factory.Services.GetRequiredService(); if (userTwoFactor != null) { user.TwoFactorProviders = userTwoFactor; @@ -426,16 +432,20 @@ public class IdentityServerTwoFactorTests : IClassFixture(); - var user = await userRepository.GetByEmailAsync(testEmail); - Assert.NotNull(user); + var user = await factory.RegisterNewIdentityFactoryUserAsync( + new RegisterFinishRequestModel + { + Email = testEmail, + MasterPasswordHash = _testPassword, + Kdf = KdfType.PBKDF2_SHA256, + KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default, + UserAsymmetricKeys = new KeysRequestModel() + { + PublicKey = "public_key", + EncryptedPrivateKey = "private_key" + }, + UserSymmetricKey = "sym_key", + }); var userService = factory.GetService(); if (userTwoFactor != null) diff --git a/test/Identity.IntegrationTest/Identity.IntegrationTest.csproj b/test/Identity.IntegrationTest/Identity.IntegrationTest.csproj index d7a7bb9a01..5c94fad1d1 100644 --- a/test/Identity.IntegrationTest/Identity.IntegrationTest.csproj +++ b/test/Identity.IntegrationTest/Identity.IntegrationTest.csproj @@ -24,6 +24,7 @@ + diff --git a/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs b/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs index 4bec8d8167..9a1b8141ae 100644 --- a/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs +++ b/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs @@ -1,11 +1,11 @@ using System.Text.Json; +using Bit.Core; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Repositories; -using Bit.Core.Services; -using Bit.Identity.Models.Request.Accounts; using Bit.IntegrationTestCommon.Factories; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; @@ -19,28 +19,16 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture _userManager; - private readonly IAuthRequestRepository _authRequestRepository; - private readonly IDeviceService _deviceService; - - public ResourceOwnerPasswordValidatorTests(IdentityApplicationFactory factory) - { - _factory = factory; - - _userManager = _factory.GetService>(); - _authRequestRepository = _factory.GetService(); - _deviceService = _factory.GetService(); - } [Fact] public async Task ValidateAsync_Success() { // Arrange - await EnsureUserCreatedAsync(); + var localFactory = new IdentityApplicationFactory(); + await EnsureUserCreatedAsync(localFactory); // Act - var context = await _factory.Server.PostAsync("/connect/token", + var context = await localFactory.Server.PostAsync("/connect/token", GetFormUrlEncodedContent(), context => context.SetAuthEmail(DefaultUsername)); @@ -56,10 +44,11 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture context.SetAuthEmail(username)); @@ -105,13 +96,16 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture>(); // Verify the User is not null to ensure the failure is due to bad password - Assert.NotNull(await _userManager.FindByEmailAsync(DefaultUsername)); + Assert.NotNull(await userManager.FindByEmailAsync(DefaultUsername)); // Act - var context = await _factory.Server.PostAsync("/connect/token", + var context = await localFactory.Server.PostAsync("/connect/token", GetFormUrlEncodedContent(password: badPassword), context => context.SetAuthEmail(DefaultUsername)); @@ -128,9 +122,12 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture>(); + var user = await userManager.FindByEmailAsync(DefaultUsername); Assert.NotNull(user); // Connect Request to User and set CreationDate @@ -139,13 +136,14 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture(); + await authRequestRepository.CreateAsync(authRequest); - var expectedAuthRequest = await _authRequestRepository.GetManyByUserIdAsync(user.Id); + var expectedAuthRequest = await authRequestRepository.GetManyByUserIdAsync(user.Id); Assert.NotEmpty(expectedAuthRequest); // Act - var context = await _factory.Server.PostAsync("/connect/token", + var context = await localFactory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary { { "scope", "api offline_access" }, @@ -171,9 +169,12 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture>(); + + var user = await userManager.FindByEmailAsync(DefaultUsername); Assert.NotNull(user); // Create AuthRequest @@ -184,7 +185,7 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture { { "scope", "api offline_access" }, @@ -214,19 +215,23 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture(), passwordHash, token, userGuid) - .Returns(Task.FromResult(IdentityResult.Success)); - var request = new RegisterRequestModel - { - Name = "Example User", - Email = "user@example.com", - MasterPasswordHash = passwordHash, - MasterPasswordHint = "example", - Token = token, - OrganizationUserId = userGuid - }; - - await _sut.PostRegister(request); - - await _registerUserCommand.Received(1).RegisterUserViaOrganizationInviteToken(Arg.Any(), passwordHash, token, userGuid); - } - - [Fact] - public async Task PostRegister_WhenUserServiceFails_ShouldThrowBadRequestException() - { - var passwordHash = "abcdef"; - var token = "123456"; - var userGuid = new Guid(); - _registerUserCommand.RegisterUserViaOrganizationInviteToken(Arg.Any(), passwordHash, token, userGuid) - .Returns(Task.FromResult(IdentityResult.Failed())); - var request = new RegisterRequestModel - { - Name = "Example User", - Email = "user@example.com", - MasterPasswordHash = passwordHash, - MasterPasswordHint = "example", - Token = token, - OrganizationUserId = userGuid - }; - - await Assert.ThrowsAsync(() => _sut.PostRegister(request)); - } - [Theory] [BitAutoData] public async Task PostRegisterSendEmailVerification_WhenTokenReturnedFromCommand_Returns200WithToken(string email, string name, bool receiveMarketingEmails) diff --git a/test/IntegrationTestCommon/Factories/IdentityApplicationFactory.cs b/test/IntegrationTestCommon/Factories/IdentityApplicationFactory.cs index b69a93013b..a686605836 100644 --- a/test/IntegrationTestCommon/Factories/IdentityApplicationFactory.cs +++ b/test/IntegrationTestCommon/Factories/IdentityApplicationFactory.cs @@ -1,23 +1,51 @@ -using System.Net.Http.Json; +using System.Collections.Concurrent; +using System.Net.Http.Json; using System.Text.Json; using Bit.Core.Auth.Models.Api.Request.Accounts; +using Bit.Core.Entities; using Bit.Core.Enums; +using Bit.Core.Services; using Bit.Core.Utilities; using Bit.Identity; -using Bit.Identity.Models.Request.Accounts; using Bit.Test.Common.Helpers; using HandlebarsDotNet; +using LinqToDB; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using NSubstitute; +using Xunit; namespace Bit.IntegrationTestCommon.Factories; public class IdentityApplicationFactory : WebApplicationFactoryBase { public const string DefaultDeviceIdentifier = "92b9d953-b9b6-4eaf-9d3e-11d57144dfeb"; + public const string DefaultUserEmail = "DefaultEmail@bitwarden.com"; + public const string DefaultUserPasswordHash = "default_password_hash"; - public async Task RegisterAsync(RegisterRequestModel model) + /// + /// A dictionary to store registration tokens for email verification. We cannot substitute the IMailService more than once, so + /// we capture the email tokens for new user registration in the constructor. The email must be unique otherwise an error will be thrown. + /// + public ConcurrentDictionary RegistrationTokens { get; private set; } = new ConcurrentDictionary(); + + protected override void ConfigureWebHost(IWebHostBuilder builder) { - return await Server.PostAsync("/accounts/register", JsonContent.Create(model)); + // This allows us to use the official registration flow + SubstituteService(service => + { + service.SendRegistrationVerificationEmailAsync(Arg.Any(), Arg.Any()) + .ReturnsForAnyArgs(Task.CompletedTask) + .AndDoes(call => + { + if (!RegistrationTokens.TryAdd(call.ArgAt(0), call.ArgAt(1))) + { + throw new InvalidOperationException("This email was already registered for new user registration."); + } + }); + }); + + base.ConfigureWebHost(builder); } public async Task PostRegisterSendEmailVerificationAsync(RegisterSendVerificationEmailRequestModel model) @@ -155,4 +183,42 @@ public class IdentityApplicationFactory : WebApplicationFactoryBase })); return context; } + + /// + /// Registers a new user to the Identity Application Factory based on the RegisterFinishRequestModel + /// + /// RegisterFinishRequestModel needed to seed data to the test user + /// optional parameter that is tracked during the inital steps of registration. + /// returns the newly created user + public async Task RegisterNewIdentityFactoryUserAsync( + RegisterFinishRequestModel requestModel, + bool marketingEmails = true) + { + var sendVerificationEmailReqModel = new RegisterSendVerificationEmailRequestModel + { + Email = requestModel.Email, + Name = "name", + ReceiveMarketingEmails = marketingEmails + }; + + var sendEmailVerificationResponseHttpContext = await PostRegisterSendEmailVerificationAsync(sendVerificationEmailReqModel); + + Assert.Equal(StatusCodes.Status204NoContent, sendEmailVerificationResponseHttpContext.Response.StatusCode); + Assert.NotNull(RegistrationTokens[requestModel.Email]); + + // Now we call the finish registration endpoint with the email verification token + requestModel.EmailVerificationToken = RegistrationTokens[requestModel.Email]; + + var postRegisterFinishHttpContext = await PostRegisterFinishAsync(requestModel); + + Assert.Equal(StatusCodes.Status200OK, postRegisterFinishHttpContext.Response.StatusCode); + + var database = GetDatabaseContext(); + var user = await database.Users + .SingleAsync(u => u.Email == requestModel.Email); + + Assert.NotNull(user); + + return user; + } }