1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 07:36:14 -05:00

Auth/PM-11945 - Registration with Email Verification - Fix Org Sponsored Free Family Plan not working (#4772)

* PM-11945 - Rename RegisterUserWithOptionalOrgInvite to RegisterUserViaOrgInvite as the org invite isn't optional in the function - just the overall process of registration.

* PM-11945 - Yet another rename

* PM-11945 - Wire up call to RegisterUserViaOrgSponsoredFreeFamilyPlanInviteToken and test.

* PM-11945 - RegisterUserCommandTests - test new method

* PM-11949 - Rename tests

* PM-11945 - AccountsControllerTests.cs - add integration test for RegistrationWithEmailVerification_WithOrgSponsoredFreeFamilyPlanInviteToken_Succeeds

* PM-11945 - Adjust naming per PR feedback to match docs.

* PM-11945 - More renaming
This commit is contained in:
Jared Snider
2024-09-12 15:24:47 -04:00
committed by GitHub
parent 95ba256511
commit 7d8df767cd
7 changed files with 231 additions and 29 deletions

View File

@ -7,6 +7,7 @@ using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.UserFeatures.Registration.Implementations;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
@ -84,12 +85,11 @@ public class RegisterUserCommandTests
.RaiseEventAsync(Arg.Any<ReferenceEvent>());
}
// RegisterUserWithOptionalOrgInvite tests
// RegisterUserWithOrganizationInviteToken tests
// Simple happy path test
[Theory]
[BitAutoData]
public async Task RegisterUserWithOptionalOrgInvite_NoOrgInviteOrOrgUserIdOrReferenceData_Succeeds(
public async Task RegisterUserViaOrganizationInviteToken_NoOrgInviteOrOrgUserIdOrReferenceData_Succeeds(
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash)
{
// Arrange
@ -100,7 +100,7 @@ public class RegisterUserCommandTests
.Returns(IdentityResult.Success);
// Act
var result = await sutProvider.Sut.RegisterUserWithOptionalOrgInvite(user, masterPasswordHash, null, null);
var result = await sutProvider.Sut.RegisterUserViaOrganizationInviteToken(user, masterPasswordHash, null, null);
// Assert
Assert.True(result.Succeeded);
@ -119,7 +119,7 @@ public class RegisterUserCommandTests
[BitAutoData(false, null)]
[BitAutoData(true, "sampleInitiationPath")]
[BitAutoData(true, "Secrets Manager trial")]
public async Task RegisterUserWithOptionalOrgInvite_ComplexHappyPath_Succeeds(bool addUserReferenceData, string initiationPath,
public async Task RegisterUserViaOrganizationInviteToken_ComplexHappyPath_Succeeds(bool addUserReferenceData, string initiationPath,
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid orgUserId, Policy twoFactorPolicy)
{
// Arrange
@ -158,7 +158,7 @@ public class RegisterUserCommandTests
user.ReferenceData = addUserReferenceData ? $"{{\"initiationPath\":\"{initiationPath}\"}}" : null;
// Act
var result = await sutProvider.Sut.RegisterUserWithOptionalOrgInvite(user, masterPasswordHash, orgInviteToken, orgUserId);
var result = await sutProvider.Sut.RegisterUserViaOrganizationInviteToken(user, masterPasswordHash, orgInviteToken, orgUserId);
// Assert
await sutProvider.GetDependency<IOrganizationUserRepository>()
@ -227,7 +227,7 @@ public class RegisterUserCommandTests
[BitAutoData("invalidOrgInviteToken")]
[BitAutoData("nullOrgInviteToken")]
[BitAutoData("nullOrgUserId")]
public async Task RegisterUserWithOptionalOrgInvite_MissingOrInvalidOrgInviteDataWithDisabledOpenRegistration_ThrowsBadRequestException(string scenario,
public async Task RegisterUserViaOrganizationInviteToken_MissingOrInvalidOrgInviteDataWithDisabledOpenRegistration_ThrowsBadRequestException(string scenario,
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid? orgUserId)
{
// Arrange
@ -257,7 +257,7 @@ public class RegisterUserCommandTests
}
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RegisterUserWithOptionalOrgInvite(user, masterPasswordHash, orgInviteToken, orgUserId));
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RegisterUserViaOrganizationInviteToken(user, masterPasswordHash, orgInviteToken, orgUserId));
Assert.Equal("Open registration has been disabled by the system administrator.", exception.Message);
}
@ -265,7 +265,7 @@ public class RegisterUserCommandTests
[BitAutoData("invalidOrgInviteToken")]
[BitAutoData("nullOrgInviteToken")]
[BitAutoData("nullOrgUserId")]
public async Task RegisterUserWithOptionalOrgInvite_MissingOrInvalidOrgInviteDataWithEnabledOpenRegistration_ThrowsBadRequestException(string scenario,
public async Task RegisterUserViaOrganizationInviteToken_MissingOrInvalidOrgInviteDataWithEnabledOpenRegistration_ThrowsBadRequestException(string scenario,
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid? orgUserId)
{
// Arrange
@ -307,7 +307,7 @@ public class RegisterUserCommandTests
// Act
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.RegisterUserWithOptionalOrgInvite(user, masterPasswordHash, orgInviteToken, orgUserId));
sutProvider.Sut.RegisterUserViaOrganizationInviteToken(user, masterPasswordHash, orgInviteToken, orgUserId));
Assert.Equal(expectedErrorMessage, exception.Message);
}
@ -381,4 +381,76 @@ public class RegisterUserCommandTests
}
// RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken
[Theory]
[BitAutoData]
public async Task RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken_Succeeds(SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash,
string orgSponsoredFreeFamilyPlanInviteToken)
{
// Arrange
sutProvider.GetDependency<IValidateRedemptionTokenCommand>()
.ValidateRedemptionTokenAsync(orgSponsoredFreeFamilyPlanInviteToken, user.Email)
.Returns((true, new OrganizationSponsorship()));
sutProvider.GetDependency<IUserService>()
.CreateUserAsync(user, masterPasswordHash)
.Returns(IdentityResult.Success);
// Act
var result = await sutProvider.Sut.RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(user, masterPasswordHash, orgSponsoredFreeFamilyPlanInviteToken);
// Assert
Assert.True(result.Succeeded);
await sutProvider.GetDependency<IUserService>()
.Received(1)
.CreateUserAsync(Arg.Is<User>(u => u.Name == user.Name && u.EmailVerified == true && u.ApiKey != null), masterPasswordHash);
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendWelcomeEmailAsync(user);
await sutProvider.GetDependency<IReferenceEventService>()
.Received(1)
.RaiseEventAsync(Arg.Is<ReferenceEvent>(refEvent => refEvent.Type == ReferenceEventType.Signup));
}
[Theory]
[BitAutoData]
public async Task RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken_InvalidToken_ThrowsBadRequestException(SutProvider<RegisterUserCommand> sutProvider, User user,
string masterPasswordHash, string orgSponsoredFreeFamilyPlanInviteToken)
{
// Arrange
sutProvider.GetDependency<IValidateRedemptionTokenCommand>()
.ValidateRedemptionTokenAsync(orgSponsoredFreeFamilyPlanInviteToken, user.Email)
.Returns((false, new OrganizationSponsorship()));
// Act & Assert
var result = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(user, masterPasswordHash, orgSponsoredFreeFamilyPlanInviteToken));
Assert.Equal("Invalid org sponsored free family plan token.", result.Message);
}
[Theory]
[BitAutoData]
public async Task RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken_DisabledOpenRegistration_ThrowsBadRequestException(SutProvider<RegisterUserCommand> sutProvider, User user,
string masterPasswordHash, string orgSponsoredFreeFamilyPlanInviteToken)
{
// Arrange
sutProvider.GetDependency<IGlobalSettings>()
.DisableUserRegistration = true;
// Act & Assert
var result = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(user, masterPasswordHash, orgSponsoredFreeFamilyPlanInviteToken));
Assert.Equal("Open registration has been disabled by the system administrator.", result.Message);
}
}

View File

@ -4,6 +4,7 @@ using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Auth.Models.Business.Tokenables;
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;
@ -322,6 +323,84 @@ public class AccountsControllerTests : IClassFixture<IdentityApplicationFactory>
Assert.Equal(kdfParallelism, user.KdfParallelism);
}
[Theory, BitAutoData]
public async Task RegistrationWithEmailVerification_WithOrgSponsoredFreeFamilyPlanInviteToken_Succeeds(
[StringLength(1000)] string masterPasswordHash, [StringLength(50)] string masterPasswordHint, string userSymmetricKey,
KeysRequestModel userAsymmetricKeys, int kdfMemory, int kdfParallelism, Guid orgSponsorshipId)
{
// Localize factory to just this test.
var localFactory = new IdentityApplicationFactory();
// Hardcoded, valid org sponsored free family plan invite token data
var email = "jsnider+local10000008@bitwarden.com";
var orgSponsoredFreeFamilyPlanToken = "BWOrganizationSponsorship_CfDJ8HFsgwUNr89EtnCal5H72cx11wdMdD5_FSNMJoXJKp9migo8ZXi2Qx8GOM2b8IccesQEvZxzX_VDvhaaFi1NZc7-5bdadsfaPiwvzy28qwaW5-iF72vncmixArxKt8_FrJCqvn-5Yh45DvUWeOUBl1fPPx6LB4lgf6DcFkFZaHKOxIEywkFWEX9IWsLAfBfhU9K7AYZ02kxLRgXDK_eH3SKY0luoyUbRLBJRq1J9WnAQNcPLx9GOywQDUGRNvQGYmrzpAdq8y3MgUby_XD2NBf4-Vfr_0DIYPlGVJz0Ab1CwKbQ5G9vTXrFbbHQni40GVgohTq6WeVwk-PBMW9kjBw2rHO8QzWUb4whn831y-dEC";
var orgSponsorship = new OrganizationSponsorship
{
Id = orgSponsorshipId,
PlanSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise,
OfferedToEmail = email
};
var orgSponsorshipOfferTokenable = new OrganizationSponsorshipOfferTokenable(orgSponsorship) { };
localFactory.SubstituteService<IDataProtectorTokenFactory<OrganizationSponsorshipOfferTokenable>>(dataProtectorTokenFactory =>
{
dataProtectorTokenFactory.TryUnprotect(Arg.Is(orgSponsoredFreeFamilyPlanToken), out Arg.Any<OrganizationSponsorshipOfferTokenable>())
.Returns(callInfo =>
{
callInfo[1] = orgSponsorshipOfferTokenable;
return true;
});
});
localFactory.SubstituteService<IOrganizationSponsorshipRepository>(organizationSponsorshipRepository =>
{
organizationSponsorshipRepository.GetByIdAsync(orgSponsorshipId)
.Returns(orgSponsorship);
});
var registerFinishReqModel = new RegisterFinishRequestModel
{
Email = email,
MasterPasswordHash = masterPasswordHash,
MasterPasswordHint = masterPasswordHint,
OrgSponsoredFreeFamilyPlanToken = orgSponsoredFreeFamilyPlanToken,
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
UserSymmetricKey = userSymmetricKey,
UserAsymmetricKeys = userAsymmetricKeys,
KdfMemory = kdfMemory,
KdfParallelism = kdfParallelism
};
var postRegisterFinishHttpContext = await localFactory.PostRegisterFinishAsync(registerFinishReqModel);
Assert.Equal(StatusCodes.Status200OK, postRegisterFinishHttpContext.Response.StatusCode);
var database = localFactory.GetDatabaseContext();
var user = await database.Users
.SingleAsync(u => u.Email == email);
Assert.NotNull(user);
// Assert user properties match the request model
Assert.Equal(email, user.Email);
Assert.NotEqual(masterPasswordHash, user.MasterPassword); // We execute server side hashing
Assert.NotNull(user.MasterPassword);
Assert.Equal(masterPasswordHint, user.MasterPasswordHint);
Assert.Equal(userSymmetricKey, user.Key);
Assert.Equal(userAsymmetricKeys.EncryptedPrivateKey, user.PrivateKey);
Assert.Equal(userAsymmetricKeys.PublicKey, user.PublicKey);
Assert.Equal(KdfType.PBKDF2_SHA256, user.Kdf);
Assert.Equal(AuthConstants.PBKDF2_ITERATIONS.Default, user.KdfIterations);
Assert.Equal(kdfMemory, user.KdfMemory);
Assert.Equal(kdfParallelism, user.KdfParallelism);
}
[Theory, BitAutoData]
public async Task PostRegisterVerificationEmailClicked_Success(
[Required, StringLength(20)] string name,

View File

@ -111,7 +111,7 @@ public class AccountsControllerTests : IDisposable
var passwordHash = "abcdef";
var token = "123456";
var userGuid = new Guid();
_registerUserCommand.RegisterUserWithOptionalOrgInvite(Arg.Any<User>(), passwordHash, token, userGuid)
_registerUserCommand.RegisterUserViaOrganizationInviteToken(Arg.Any<User>(), passwordHash, token, userGuid)
.Returns(Task.FromResult(IdentityResult.Success));
var request = new RegisterRequestModel
{
@ -125,7 +125,7 @@ public class AccountsControllerTests : IDisposable
await _sut.PostRegister(request);
await _registerUserCommand.Received(1).RegisterUserWithOptionalOrgInvite(Arg.Any<User>(), passwordHash, token, userGuid);
await _registerUserCommand.Received(1).RegisterUserViaOrganizationInviteToken(Arg.Any<User>(), passwordHash, token, userGuid);
}
[Fact]
@ -134,7 +134,7 @@ public class AccountsControllerTests : IDisposable
var passwordHash = "abcdef";
var token = "123456";
var userGuid = new Guid();
_registerUserCommand.RegisterUserWithOptionalOrgInvite(Arg.Any<User>(), passwordHash, token, userGuid)
_registerUserCommand.RegisterUserViaOrganizationInviteToken(Arg.Any<User>(), passwordHash, token, userGuid)
.Returns(Task.FromResult(IdentityResult.Failed()));
var request = new RegisterRequestModel
{
@ -219,7 +219,7 @@ public class AccountsControllerTests : IDisposable
var user = model.ToUser();
_registerUserCommand.RegisterUserWithOptionalOrgInvite(Arg.Any<User>(), masterPasswordHash, orgInviteToken, organizationUserId)
_registerUserCommand.RegisterUserViaOrganizationInviteToken(Arg.Any<User>(), masterPasswordHash, orgInviteToken, organizationUserId)
.Returns(Task.FromResult(IdentityResult.Success));
// Act
@ -227,7 +227,7 @@ public class AccountsControllerTests : IDisposable
// Assert
Assert.NotNull(result);
await _registerUserCommand.Received(1).RegisterUserWithOptionalOrgInvite(Arg.Is<User>(u =>
await _registerUserCommand.Received(1).RegisterUserViaOrganizationInviteToken(Arg.Is<User>(u =>
u.Email == user.Email &&
u.MasterPasswordHint == user.MasterPasswordHint &&
u.Kdf == user.Kdf &&
@ -270,7 +270,7 @@ public class AccountsControllerTests : IDisposable
new IdentityError { Code = duplicateUserEmailErrorCode, Description = duplicateUserEmailErrorDesc }
);
_registerUserCommand.RegisterUserWithOptionalOrgInvite(Arg.Is<User>(u =>
_registerUserCommand.RegisterUserViaOrganizationInviteToken(Arg.Is<User>(u =>
u.Email == user.Email &&
u.MasterPasswordHint == user.MasterPasswordHint &&
u.Kdf == user.Kdf &&