mirror of
https://github.com/bitwarden/server.git
synced 2024-11-28 13:15:12 +01: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:
parent
95ba256511
commit
7d8df767cd
@ -31,6 +31,7 @@ public class RegisterFinishRequestModel : IValidatableObject
|
|||||||
public Guid? OrganizationUserId { get; set; }
|
public Guid? OrganizationUserId { get; set; }
|
||||||
public string? OrgInviteToken { get; set; }
|
public string? OrgInviteToken { get; set; }
|
||||||
|
|
||||||
|
public string? OrgSponsoredFreeFamilyPlanToken { get; set; }
|
||||||
|
|
||||||
public User ToUser()
|
public User ToUser()
|
||||||
{
|
{
|
||||||
|
@ -24,7 +24,7 @@ public interface IRegisterUserCommand
|
|||||||
/// <param name="orgInviteToken">The org invite token sent to the user via email</param>
|
/// <param name="orgInviteToken">The org invite token sent to the user via email</param>
|
||||||
/// <param name="orgUserId">The associated org user guid that was created at the time of invite</param>
|
/// <param name="orgUserId">The associated org user guid that was created at the time of invite</param>
|
||||||
/// <returns><see cref="IdentityResult"/></returns>
|
/// <returns><see cref="IdentityResult"/></returns>
|
||||||
public Task<IdentityResult> RegisterUserWithOptionalOrgInvite(User user, string masterPasswordHash, string orgInviteToken, Guid? orgUserId);
|
public Task<IdentityResult> RegisterUserViaOrganizationInviteToken(User user, string masterPasswordHash, string orgInviteToken, Guid? orgUserId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new user with a given master password hash, sends a welcome email, and raises the signup reference event.
|
/// Creates a new user with a given master password hash, sends a welcome email, and raises the signup reference event.
|
||||||
@ -37,4 +37,6 @@ public interface IRegisterUserCommand
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public Task<IdentityResult> RegisterUserViaEmailVerificationToken(User user, string masterPasswordHash, string emailVerificationToken);
|
public Task<IdentityResult> RegisterUserViaEmailVerificationToken(User user, string masterPasswordHash, string emailVerificationToken);
|
||||||
|
|
||||||
|
public Task<IdentityResult> RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(User user, string masterPasswordHash, string orgSponsoredFreeFamilyPlanInviteToken);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ using Bit.Core.Auth.Models.Business.Tokenables;
|
|||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
@ -37,6 +38,8 @@ public class RegisterUserCommand : IRegisterUserCommand
|
|||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
private readonly IMailService _mailService;
|
private readonly IMailService _mailService;
|
||||||
|
|
||||||
|
private readonly IValidateRedemptionTokenCommand _validateRedemptionTokenCommand;
|
||||||
|
|
||||||
private readonly string _disabledUserRegistrationExceptionMsg = "Open registration has been disabled by the system administrator.";
|
private readonly string _disabledUserRegistrationExceptionMsg = "Open registration has been disabled by the system administrator.";
|
||||||
|
|
||||||
public RegisterUserCommand(
|
public RegisterUserCommand(
|
||||||
@ -49,7 +52,8 @@ public class RegisterUserCommand : IRegisterUserCommand
|
|||||||
IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> registrationEmailVerificationTokenDataFactory,
|
IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> registrationEmailVerificationTokenDataFactory,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
IMailService mailService
|
IMailService mailService,
|
||||||
|
IValidateRedemptionTokenCommand validateRedemptionTokenCommand
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
@ -66,6 +70,7 @@ public class RegisterUserCommand : IRegisterUserCommand
|
|||||||
_userService = userService;
|
_userService = userService;
|
||||||
_mailService = mailService;
|
_mailService = mailService;
|
||||||
|
|
||||||
|
_validateRedemptionTokenCommand = validateRedemptionTokenCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -81,8 +86,7 @@ public class RegisterUserCommand : IRegisterUserCommand
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IdentityResult> RegisterUserViaOrganizationInviteToken(User user, string masterPasswordHash,
|
||||||
public async Task<IdentityResult> RegisterUserWithOptionalOrgInvite(User user, string masterPasswordHash,
|
|
||||||
string orgInviteToken, Guid? orgUserId)
|
string orgInviteToken, Guid? orgUserId)
|
||||||
{
|
{
|
||||||
ValidateOrgInviteToken(orgInviteToken, orgUserId, user);
|
ValidateOrgInviteToken(orgInviteToken, orgUserId, user);
|
||||||
@ -233,13 +237,8 @@ public class RegisterUserCommand : IRegisterUserCommand
|
|||||||
public async Task<IdentityResult> RegisterUserViaEmailVerificationToken(User user, string masterPasswordHash,
|
public async Task<IdentityResult> RegisterUserViaEmailVerificationToken(User user, string masterPasswordHash,
|
||||||
string emailVerificationToken)
|
string emailVerificationToken)
|
||||||
{
|
{
|
||||||
// We validate open registration on send of initial email and here b/c a user could technically start the
|
|
||||||
// account creation process while open registration is enabled and then finish it after it has been
|
ValidateOpenRegistrationAllowed();
|
||||||
// disabled by the self hosted admin.
|
|
||||||
if (_globalSettings.DisableUserRegistration)
|
|
||||||
{
|
|
||||||
throw new BadRequestException(_disabledUserRegistrationExceptionMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
var tokenable = ValidateRegistrationEmailVerificationTokenable(emailVerificationToken, user.Email);
|
var tokenable = ValidateRegistrationEmailVerificationTokenable(emailVerificationToken, user.Email);
|
||||||
|
|
||||||
@ -260,6 +259,48 @@ public class RegisterUserCommand : IRegisterUserCommand
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IdentityResult> RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(User user, string masterPasswordHash,
|
||||||
|
string orgSponsoredFreeFamilyPlanInviteToken)
|
||||||
|
{
|
||||||
|
ValidateOpenRegistrationAllowed();
|
||||||
|
await ValidateOrgSponsoredFreeFamilyPlanInviteToken(orgSponsoredFreeFamilyPlanInviteToken, user.Email);
|
||||||
|
|
||||||
|
user.EmailVerified = true;
|
||||||
|
user.ApiKey = CoreHelpers.SecureRandomString(30); // API key can't be null.
|
||||||
|
|
||||||
|
var result = await _userService.CreateUserAsync(user, masterPasswordHash);
|
||||||
|
if (result == IdentityResult.Success)
|
||||||
|
{
|
||||||
|
await _mailService.SendWelcomeEmailAsync(user);
|
||||||
|
await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.Signup, user, _currentContext));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ValidateOpenRegistrationAllowed()
|
||||||
|
{
|
||||||
|
// We validate open registration on send of initial email and here b/c a user could technically start the
|
||||||
|
// account creation process while open registration is enabled and then finish it after it has been
|
||||||
|
// disabled by the self hosted admin.Ï
|
||||||
|
if (_globalSettings.DisableUserRegistration)
|
||||||
|
{
|
||||||
|
throw new BadRequestException(_disabledUserRegistrationExceptionMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ValidateOrgSponsoredFreeFamilyPlanInviteToken(string orgSponsoredFreeFamilyPlanInviteToken, string userEmail)
|
||||||
|
{
|
||||||
|
var (valid, sponsorship) = await _validateRedemptionTokenCommand.ValidateRedemptionTokenAsync(orgSponsoredFreeFamilyPlanInviteToken, userEmail);
|
||||||
|
|
||||||
|
if (!valid)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Invalid org sponsored free family plan token.");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private RegistrationEmailVerificationTokenable ValidateRegistrationEmailVerificationTokenable(string emailVerificationToken, string userEmail)
|
private RegistrationEmailVerificationTokenable ValidateRegistrationEmailVerificationTokenable(string emailVerificationToken, string userEmail)
|
||||||
{
|
{
|
||||||
_registrationEmailVerificationTokenDataFactory.TryUnprotect(emailVerificationToken, out var tokenable);
|
_registrationEmailVerificationTokenDataFactory.TryUnprotect(emailVerificationToken, out var tokenable);
|
||||||
|
@ -75,7 +75,7 @@ public class AccountsController : Controller
|
|||||||
public async Task<RegisterResponseModel> PostRegister([FromBody] RegisterRequestModel model)
|
public async Task<RegisterResponseModel> PostRegister([FromBody] RegisterRequestModel model)
|
||||||
{
|
{
|
||||||
var user = model.ToUser();
|
var user = model.ToUser();
|
||||||
var identityResult = await _registerUserCommand.RegisterUserWithOptionalOrgInvite(user, model.MasterPasswordHash,
|
var identityResult = await _registerUserCommand.RegisterUserViaOrganizationInviteToken(user, model.MasterPasswordHash,
|
||||||
model.Token, model.OrganizationUserId);
|
model.Token, model.OrganizationUserId);
|
||||||
// delaysEnabled false is only for the new registration with email verification process
|
// delaysEnabled false is only for the new registration with email verification process
|
||||||
return await ProcessRegistrationResult(identityResult, user, delaysEnabled: true);
|
return await ProcessRegistrationResult(identityResult, user, delaysEnabled: true);
|
||||||
@ -151,12 +151,19 @@ public class AccountsController : Controller
|
|||||||
|
|
||||||
if (!string.IsNullOrEmpty(model.OrgInviteToken) && model.OrganizationUserId.HasValue)
|
if (!string.IsNullOrEmpty(model.OrgInviteToken) && model.OrganizationUserId.HasValue)
|
||||||
{
|
{
|
||||||
identityResult = await _registerUserCommand.RegisterUserWithOptionalOrgInvite(user, model.MasterPasswordHash,
|
identityResult = await _registerUserCommand.RegisterUserViaOrganizationInviteToken(user, model.MasterPasswordHash,
|
||||||
model.OrgInviteToken, model.OrganizationUserId);
|
model.OrgInviteToken, model.OrganizationUserId);
|
||||||
|
|
||||||
return await ProcessRegistrationResult(identityResult, user, delaysEnabled);
|
return await ProcessRegistrationResult(identityResult, user, delaysEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(model.OrgSponsoredFreeFamilyPlanToken))
|
||||||
|
{
|
||||||
|
identityResult = await _registerUserCommand.RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(user, model.MasterPasswordHash, model.OrgSponsoredFreeFamilyPlanToken);
|
||||||
|
|
||||||
|
return await ProcessRegistrationResult(identityResult, user, delaysEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
identityResult = await _registerUserCommand.RegisterUserViaEmailVerificationToken(user, model.MasterPasswordHash, model.EmailVerificationToken);
|
identityResult = await _registerUserCommand.RegisterUserViaEmailVerificationToken(user, model.MasterPasswordHash, model.EmailVerificationToken);
|
||||||
|
|
||||||
return await ProcessRegistrationResult(identityResult, user, delaysEnabled);
|
return await ProcessRegistrationResult(identityResult, user, delaysEnabled);
|
||||||
|
@ -7,6 +7,7 @@ using Bit.Core.Auth.Models.Business.Tokenables;
|
|||||||
using Bit.Core.Auth.UserFeatures.Registration.Implementations;
|
using Bit.Core.Auth.UserFeatures.Registration.Implementations;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
@ -84,12 +85,11 @@ public class RegisterUserCommandTests
|
|||||||
.RaiseEventAsync(Arg.Any<ReferenceEvent>());
|
.RaiseEventAsync(Arg.Any<ReferenceEvent>());
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterUserWithOptionalOrgInvite tests
|
// RegisterUserWithOrganizationInviteToken tests
|
||||||
|
|
||||||
// Simple happy path test
|
// Simple happy path test
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task RegisterUserWithOptionalOrgInvite_NoOrgInviteOrOrgUserIdOrReferenceData_Succeeds(
|
public async Task RegisterUserViaOrganizationInviteToken_NoOrgInviteOrOrgUserIdOrReferenceData_Succeeds(
|
||||||
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash)
|
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@ -100,7 +100,7 @@ public class RegisterUserCommandTests
|
|||||||
.Returns(IdentityResult.Success);
|
.Returns(IdentityResult.Success);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await sutProvider.Sut.RegisterUserWithOptionalOrgInvite(user, masterPasswordHash, null, null);
|
var result = await sutProvider.Sut.RegisterUserViaOrganizationInviteToken(user, masterPasswordHash, null, null);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.True(result.Succeeded);
|
Assert.True(result.Succeeded);
|
||||||
@ -119,7 +119,7 @@ public class RegisterUserCommandTests
|
|||||||
[BitAutoData(false, null)]
|
[BitAutoData(false, null)]
|
||||||
[BitAutoData(true, "sampleInitiationPath")]
|
[BitAutoData(true, "sampleInitiationPath")]
|
||||||
[BitAutoData(true, "Secrets Manager trial")]
|
[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)
|
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid orgUserId, Policy twoFactorPolicy)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@ -158,7 +158,7 @@ public class RegisterUserCommandTests
|
|||||||
user.ReferenceData = addUserReferenceData ? $"{{\"initiationPath\":\"{initiationPath}\"}}" : null;
|
user.ReferenceData = addUserReferenceData ? $"{{\"initiationPath\":\"{initiationPath}\"}}" : null;
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await sutProvider.Sut.RegisterUserWithOptionalOrgInvite(user, masterPasswordHash, orgInviteToken, orgUserId);
|
var result = await sutProvider.Sut.RegisterUserViaOrganizationInviteToken(user, masterPasswordHash, orgInviteToken, orgUserId);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
@ -227,7 +227,7 @@ public class RegisterUserCommandTests
|
|||||||
[BitAutoData("invalidOrgInviteToken")]
|
[BitAutoData("invalidOrgInviteToken")]
|
||||||
[BitAutoData("nullOrgInviteToken")]
|
[BitAutoData("nullOrgInviteToken")]
|
||||||
[BitAutoData("nullOrgUserId")]
|
[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)
|
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid? orgUserId)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@ -257,7 +257,7 @@ public class RegisterUserCommandTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Act & Assert
|
// 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);
|
Assert.Equal("Open registration has been disabled by the system administrator.", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -265,7 +265,7 @@ public class RegisterUserCommandTests
|
|||||||
[BitAutoData("invalidOrgInviteToken")]
|
[BitAutoData("invalidOrgInviteToken")]
|
||||||
[BitAutoData("nullOrgInviteToken")]
|
[BitAutoData("nullOrgInviteToken")]
|
||||||
[BitAutoData("nullOrgUserId")]
|
[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)
|
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid? orgUserId)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@ -307,7 +307,7 @@ public class RegisterUserCommandTests
|
|||||||
|
|
||||||
// Act
|
// Act
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
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);
|
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);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ using Bit.Core.Auth.Models.Api.Request.Accounts;
|
|||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Business.Tokenables;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Tokens;
|
using Bit.Core.Tokens;
|
||||||
@ -322,6 +323,84 @@ public class AccountsControllerTests : IClassFixture<IdentityApplicationFactory>
|
|||||||
Assert.Equal(kdfParallelism, user.KdfParallelism);
|
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]
|
[Theory, BitAutoData]
|
||||||
public async Task PostRegisterVerificationEmailClicked_Success(
|
public async Task PostRegisterVerificationEmailClicked_Success(
|
||||||
[Required, StringLength(20)] string name,
|
[Required, StringLength(20)] string name,
|
||||||
|
@ -111,7 +111,7 @@ public class AccountsControllerTests : IDisposable
|
|||||||
var passwordHash = "abcdef";
|
var passwordHash = "abcdef";
|
||||||
var token = "123456";
|
var token = "123456";
|
||||||
var userGuid = new Guid();
|
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));
|
.Returns(Task.FromResult(IdentityResult.Success));
|
||||||
var request = new RegisterRequestModel
|
var request = new RegisterRequestModel
|
||||||
{
|
{
|
||||||
@ -125,7 +125,7 @@ public class AccountsControllerTests : IDisposable
|
|||||||
|
|
||||||
await _sut.PostRegister(request);
|
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]
|
[Fact]
|
||||||
@ -134,7 +134,7 @@ public class AccountsControllerTests : IDisposable
|
|||||||
var passwordHash = "abcdef";
|
var passwordHash = "abcdef";
|
||||||
var token = "123456";
|
var token = "123456";
|
||||||
var userGuid = new Guid();
|
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()));
|
.Returns(Task.FromResult(IdentityResult.Failed()));
|
||||||
var request = new RegisterRequestModel
|
var request = new RegisterRequestModel
|
||||||
{
|
{
|
||||||
@ -219,7 +219,7 @@ public class AccountsControllerTests : IDisposable
|
|||||||
|
|
||||||
var user = model.ToUser();
|
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));
|
.Returns(Task.FromResult(IdentityResult.Success));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@ -227,7 +227,7 @@ public class AccountsControllerTests : IDisposable
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.NotNull(result);
|
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.Email == user.Email &&
|
||||||
u.MasterPasswordHint == user.MasterPasswordHint &&
|
u.MasterPasswordHint == user.MasterPasswordHint &&
|
||||||
u.Kdf == user.Kdf &&
|
u.Kdf == user.Kdf &&
|
||||||
@ -270,7 +270,7 @@ public class AccountsControllerTests : IDisposable
|
|||||||
new IdentityError { Code = duplicateUserEmailErrorCode, Description = duplicateUserEmailErrorDesc }
|
new IdentityError { Code = duplicateUserEmailErrorCode, Description = duplicateUserEmailErrorDesc }
|
||||||
);
|
);
|
||||||
|
|
||||||
_registerUserCommand.RegisterUserWithOptionalOrgInvite(Arg.Is<User>(u =>
|
_registerUserCommand.RegisterUserViaOrganizationInviteToken(Arg.Is<User>(u =>
|
||||||
u.Email == user.Email &&
|
u.Email == user.Email &&
|
||||||
u.MasterPasswordHint == user.MasterPasswordHint &&
|
u.MasterPasswordHint == user.MasterPasswordHint &&
|
||||||
u.Kdf == user.Kdf &&
|
u.Kdf == user.Kdf &&
|
||||||
|
Loading…
Reference in New Issue
Block a user