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;
+ }
}