mirror of
https://github.com/bitwarden/server.git
synced 2025-04-05 05:00:19 -05:00

* PM-7322 - AccountsController.cs - create empty method + empty req model to be able to create draft PR. * PM-7322 - Start on RegisterFinishRequestModel.cs * PM-7322 - WIP on Complete Registration endpoint * PM-7322 - UserService.cs - RegisterUserAsync - Tweak of token to be orgInviteToken as we are adding a new email verification token to the mix. * PM-7322 - UserService - Rename MP to MPHash * PM-7322 - More WIP progress on getting new finish registration process in place. * PM-7322 Create IRegisterUserCommand * PM-7322 - RegisterUserCommand.cs - first WIP draft * PM-7322 - Implement use of new command in Identity. * PM-7322 - Rename RegisterUserViaOrgInvite to just be RegisterUser as orgInvite is optional. * PM07322 - Test RegisterUserCommand.RegisterUser(...) happy paths and one bad request path. * PM-7322 - More WIP on RegisterUserCommand.cs and tests * PM-7322 - RegisterUserCommand.cs - refactor ValidateOrgInviteToken logic to always validate the token if we have one. * PM-7322 - RegisterUserCommand.cs - Refactor OrgInviteToken validation to be more clear + validate org invite token even in open registration scenarios + added tests. * PM-7322 - Add more test coverage to RegisterUserWithOptionalOrgInvite * PM-7322 - IRegisterUserCommand - DOCS * PM-7322 - Test RegisterUser * PM-7322 - IRegisterUserCommand - Add more docs. * PM-7322 - Finish updating all existing user service register calls to use the new command. * PM-7322 - RegistrationEmailVerificationTokenable.cs changes + tests * PM-7322 - RegistrationEmailVerificationTokenable.cs changed to only verify email as it's the only thing we need to verify + updated tests. * PM-7322 - Get RegisterUserViaEmailVerificationToken built and tested * PM-7322 - AccountsController.cs - get bones of PostRegisterFinish in place * PM-7322 - SendVerificationEmailForRegistrationCommand - Feature flag timing attack delays per architecture discussion with a default of keeping them around. * PM-7322 - RegisterFinishRequestModel.cs - EmailVerificationToken must be optional for org invite scenarios. * PM-7322 - HandlebarsMailService.cs - SendRegistrationVerificationEmailAsync - must URL encode email to avoid invalid email upon submission to server on complete registration step * PM-7322 - RegisterUserCommandTests.cs - add API key assertions * PM-7322 - Clean up RegisterUserCommand.cs * PM-7322 - Refactor AccountsController.cs existing org invite method and new process to consider new feature flag for delays. * PM-7322 - Add feature flag svc to AccountsControllerTests.cs + add TODO * PM-7322 - AccountsController.cs - Refactor shared IdentityResult logic into private helper. * PM-7322 - Work on getting PostRegisterFinish tests in place. * PM-7322 - AccountsControllerTests.cs - test new method. * PM-7322 - RegisterFinishRequestModel.cs - Update to use required keyword instead of required annotations as it is easier to catch mistakes. * PM-7322 - Fix misspelling * PM-7322 - Integration tests for RegistrationWithEmailVerification * PM-7322 - Fix leaky integration tests. * PM-7322 - Another leaky test fix. * PM-7322 - AccountsControllerTests.cs - fix RegistrationWithEmailVerification_WithOrgInviteToken_Succeeds * PM-7322 - AccountsControllerTests.cs - Finish out integration test suite!
371 lines
14 KiB
C#
371 lines
14 KiB
C#
using Bit.Core.AdminConsole.Entities;
|
|
using Bit.Core.AdminConsole.Enums;
|
|
using Bit.Core.AdminConsole.Repositories;
|
|
using Bit.Core.Auth.Enums;
|
|
using Bit.Core.Auth.Models;
|
|
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.Repositories;
|
|
using Bit.Core.Services;
|
|
using Bit.Core.Settings;
|
|
using Bit.Core.Tokens;
|
|
using Bit.Core.Tools.Enums;
|
|
using Bit.Core.Tools.Models.Business;
|
|
using Bit.Core.Tools.Services;
|
|
using Bit.Core.Utilities;
|
|
using Bit.Test.Common.AutoFixture;
|
|
using Bit.Test.Common.AutoFixture.Attributes;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using NSubstitute;
|
|
using Xunit;
|
|
|
|
namespace Bit.Core.Test.Auth.UserFeatures.Registration;
|
|
|
|
[SutProviderCustomize]
|
|
public class RegisterUserCommandTests
|
|
{
|
|
|
|
// RegisterUser tests
|
|
[Theory]
|
|
[BitAutoData]
|
|
public async Task RegisterUser_Succeeds(SutProvider<RegisterUserCommand> sutProvider, User user)
|
|
{
|
|
// Arrange
|
|
sutProvider.GetDependency<IUserService>()
|
|
.CreateUserAsync(user)
|
|
.Returns(IdentityResult.Success);
|
|
|
|
// Act
|
|
var result = await sutProvider.Sut.RegisterUser(user);
|
|
|
|
// Assert
|
|
Assert.True(result.Succeeded);
|
|
|
|
await sutProvider.GetDependency<IUserService>()
|
|
.Received(1)
|
|
.CreateUserAsync(user);
|
|
|
|
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 RegisterUser_WhenCreateUserFails_ReturnsIdentityResultFailed(SutProvider<RegisterUserCommand> sutProvider, User user)
|
|
{
|
|
// Arrange
|
|
sutProvider.GetDependency<IUserService>()
|
|
.CreateUserAsync(user)
|
|
.Returns(IdentityResult.Failed());
|
|
|
|
// Act
|
|
var result = await sutProvider.Sut.RegisterUser(user);
|
|
|
|
// Assert
|
|
Assert.False(result.Succeeded);
|
|
|
|
await sutProvider.GetDependency<IUserService>()
|
|
.Received(1)
|
|
.CreateUserAsync(user);
|
|
|
|
await sutProvider.GetDependency<IMailService>()
|
|
.DidNotReceive()
|
|
.SendWelcomeEmailAsync(Arg.Any<User>());
|
|
|
|
await sutProvider.GetDependency<IReferenceEventService>()
|
|
.DidNotReceive()
|
|
.RaiseEventAsync(Arg.Any<ReferenceEvent>());
|
|
}
|
|
|
|
// RegisterUserWithOptionalOrgInvite tests
|
|
|
|
// Simple happy path test
|
|
[Theory]
|
|
[BitAutoData]
|
|
public async Task RegisterUserWithOptionalOrgInvite_NoOrgInviteOrOrgUserIdOrReferenceData_Succeeds(
|
|
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash)
|
|
{
|
|
// Arrange
|
|
user.ReferenceData = null;
|
|
|
|
sutProvider.GetDependency<IUserService>()
|
|
.CreateUserAsync(user, masterPasswordHash)
|
|
.Returns(IdentityResult.Success);
|
|
|
|
// Act
|
|
var result = await sutProvider.Sut.RegisterUserWithOptionalOrgInvite(user, masterPasswordHash, null, null);
|
|
|
|
// Assert
|
|
Assert.True(result.Succeeded);
|
|
|
|
await sutProvider.GetDependency<IUserService>()
|
|
.Received(1)
|
|
.CreateUserAsync(user, masterPasswordHash);
|
|
|
|
await sutProvider.GetDependency<IReferenceEventService>()
|
|
.Received(1)
|
|
.RaiseEventAsync(Arg.Is<ReferenceEvent>(refEvent => refEvent.Type == ReferenceEventType.Signup));
|
|
}
|
|
|
|
// Complex happy path test
|
|
[Theory]
|
|
[BitAutoData(false, null)]
|
|
[BitAutoData(true, "sampleInitiationPath")]
|
|
[BitAutoData(true, "Secrets Manager trial")]
|
|
public async Task RegisterUserWithOptionalOrgInvite_ComplexHappyPath_Succeeds(bool addUserReferenceData, string initiationPath,
|
|
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid orgUserId, Policy twoFactorPolicy)
|
|
{
|
|
// Arrange
|
|
sutProvider.GetDependency<IGlobalSettings>()
|
|
.DisableUserRegistration.Returns(false);
|
|
|
|
sutProvider.GetDependency<IGlobalSettings>()
|
|
.DisableUserRegistration.Returns(true);
|
|
|
|
orgUser.Email = user.Email;
|
|
orgUser.Id = orgUserId;
|
|
|
|
var orgInviteTokenable = new OrgUserInviteTokenable(orgUser);
|
|
|
|
sutProvider.GetDependency<IDataProtectorTokenFactory<OrgUserInviteTokenable>>()
|
|
.TryUnprotect(orgInviteToken, out Arg.Any<OrgUserInviteTokenable>())
|
|
.Returns(callInfo =>
|
|
{
|
|
callInfo[1] = orgInviteTokenable;
|
|
return true;
|
|
});
|
|
|
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
|
.GetByIdAsync(orgUserId)
|
|
.Returns(orgUser);
|
|
|
|
twoFactorPolicy.Enabled = true;
|
|
sutProvider.GetDependency<IPolicyRepository>()
|
|
.GetByOrganizationIdTypeAsync(orgUser.OrganizationId, PolicyType.TwoFactorAuthentication)
|
|
.Returns(twoFactorPolicy);
|
|
|
|
sutProvider.GetDependency<IUserService>()
|
|
.CreateUserAsync(user, masterPasswordHash)
|
|
.Returns(IdentityResult.Success);
|
|
|
|
user.ReferenceData = addUserReferenceData ? $"{{\"initiationPath\":\"{initiationPath}\"}}" : null;
|
|
|
|
// Act
|
|
var result = await sutProvider.Sut.RegisterUserWithOptionalOrgInvite(user, masterPasswordHash, orgInviteToken, orgUserId);
|
|
|
|
// Assert
|
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
|
.Received(1)
|
|
.GetByIdAsync(orgUserId);
|
|
|
|
await sutProvider.GetDependency<IPolicyRepository>()
|
|
.Received(1)
|
|
.GetByOrganizationIdTypeAsync(orgUser.OrganizationId, PolicyType.TwoFactorAuthentication);
|
|
|
|
sutProvider.GetDependency<IUserService>()
|
|
.Received(1)
|
|
.SetTwoFactorProvider(user, TwoFactorProviderType.Email);
|
|
|
|
// example serialized data: {"1":{"Enabled":true,"MetaData":{"Email":"0dbf746c-deaf-4318-811e-d98ea7155075"}}}
|
|
var twoFactorProviders = new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
|
{
|
|
[TwoFactorProviderType.Email] = new TwoFactorProvider
|
|
{
|
|
MetaData = new Dictionary<string, object> { ["Email"] = user.Email.ToLowerInvariant() },
|
|
Enabled = true
|
|
}
|
|
};
|
|
|
|
var serializedTwoFactorProviders =
|
|
JsonHelpers.LegacySerialize(twoFactorProviders, JsonHelpers.LegacyEnumKeyResolver);
|
|
|
|
Assert.Equal(user.TwoFactorProviders, serializedTwoFactorProviders);
|
|
|
|
await sutProvider.GetDependency<IUserService>()
|
|
.Received(1)
|
|
.CreateUserAsync(Arg.Is<User>(u => u.EmailVerified == true && u.ApiKey != null), masterPasswordHash);
|
|
|
|
if (addUserReferenceData)
|
|
{
|
|
if (initiationPath.Contains("Secrets Manager trial"))
|
|
{
|
|
await sutProvider.GetDependency<IMailService>()
|
|
.Received(1)
|
|
.SendTrialInitiationEmailAsync(user.Email);
|
|
}
|
|
else
|
|
{
|
|
await sutProvider.GetDependency<IMailService>()
|
|
.Received(1)
|
|
.SendWelcomeEmailAsync(user);
|
|
}
|
|
|
|
await sutProvider.GetDependency<IReferenceEventService>()
|
|
.Received(1)
|
|
.RaiseEventAsync(Arg.Is<ReferenceEvent>(refEvent => refEvent.Type == ReferenceEventType.Signup && refEvent.SignupInitiationPath == initiationPath));
|
|
|
|
}
|
|
else
|
|
{
|
|
await sutProvider.GetDependency<IReferenceEventService>()
|
|
.Received(1)
|
|
.RaiseEventAsync(Arg.Is<ReferenceEvent>(refEvent => refEvent.Type == ReferenceEventType.Signup && refEvent.SignupInitiationPath == default));
|
|
}
|
|
|
|
Assert.True(result.Succeeded);
|
|
|
|
}
|
|
|
|
[Theory]
|
|
[BitAutoData("invalidOrgInviteToken")]
|
|
[BitAutoData("nullOrgInviteToken")]
|
|
[BitAutoData("nullOrgUserId")]
|
|
public async Task RegisterUserWithOptionalOrgInvite_MissingOrInvalidOrgInviteDataWithDisabledOpenRegistration_ThrowsBadRequestException(string scenario,
|
|
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid? orgUserId)
|
|
{
|
|
// Arrange
|
|
sutProvider.GetDependency<IGlobalSettings>()
|
|
.DisableUserRegistration.Returns(true);
|
|
|
|
switch (scenario)
|
|
{
|
|
case "invalidOrgInviteToken":
|
|
orgUser.Email = null; // make org user not match user and thus make tokenable invalid
|
|
var orgInviteTokenable = new OrgUserInviteTokenable(orgUser);
|
|
|
|
sutProvider.GetDependency<IDataProtectorTokenFactory<OrgUserInviteTokenable>>()
|
|
.TryUnprotect(orgInviteToken, out Arg.Any<OrgUserInviteTokenable>())
|
|
.Returns(callInfo =>
|
|
{
|
|
callInfo[1] = orgInviteTokenable;
|
|
return true;
|
|
});
|
|
break;
|
|
case "nullOrgInviteToken":
|
|
orgInviteToken = null;
|
|
break;
|
|
case "nullOrgUserId":
|
|
orgUserId = default;
|
|
break;
|
|
}
|
|
|
|
// Act & Assert
|
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RegisterUserWithOptionalOrgInvite(user, masterPasswordHash, orgInviteToken, orgUserId));
|
|
Assert.Equal("Open registration has been disabled by the system administrator.", exception.Message);
|
|
}
|
|
|
|
[Theory]
|
|
[BitAutoData("invalidOrgInviteToken")]
|
|
[BitAutoData("nullOrgInviteToken")]
|
|
[BitAutoData("nullOrgUserId")]
|
|
public async Task RegisterUserWithOptionalOrgInvite_MissingOrInvalidOrgInviteDataWithEnabledOpenRegistration_ThrowsBadRequestException(string scenario,
|
|
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid? orgUserId)
|
|
{
|
|
// Arrange
|
|
sutProvider.GetDependency<IGlobalSettings>()
|
|
.DisableUserRegistration.Returns(false);
|
|
|
|
string expectedErrorMessage = null;
|
|
switch (scenario)
|
|
{
|
|
case "invalidOrgInviteToken":
|
|
orgUser.Email = null; // make org user not match user and thus make tokenable invalid
|
|
var orgInviteTokenable = new OrgUserInviteTokenable(orgUser);
|
|
|
|
sutProvider.GetDependency<IDataProtectorTokenFactory<OrgUserInviteTokenable>>()
|
|
.TryUnprotect(orgInviteToken, out Arg.Any<OrgUserInviteTokenable>())
|
|
.Returns(callInfo =>
|
|
{
|
|
callInfo[1] = orgInviteTokenable;
|
|
return true;
|
|
});
|
|
|
|
expectedErrorMessage = "Organization invite token is invalid.";
|
|
break;
|
|
case "nullOrgInviteToken":
|
|
orgInviteToken = null;
|
|
expectedErrorMessage = "Organization user id cannot be provided without an organization invite token.";
|
|
break;
|
|
case "nullOrgUserId":
|
|
orgUserId = default;
|
|
expectedErrorMessage = "Organization invite token cannot be validated without an organization user id.";
|
|
break;
|
|
}
|
|
|
|
user.ReferenceData = null;
|
|
|
|
sutProvider.GetDependency<IUserService>()
|
|
.CreateUserAsync(user, masterPasswordHash)
|
|
.Returns(IdentityResult.Success);
|
|
|
|
// Act
|
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
|
sutProvider.Sut.RegisterUserWithOptionalOrgInvite(user, masterPasswordHash, orgInviteToken, orgUserId));
|
|
Assert.Equal(expectedErrorMessage, exception.Message);
|
|
}
|
|
|
|
// RegisterUserViaEmailVerificationToken
|
|
[Theory]
|
|
[BitAutoData]
|
|
public async Task RegisterUserViaEmailVerificationToken_Succeeds(SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, string emailVerificationToken, bool receiveMarketingMaterials)
|
|
{
|
|
// Arrange
|
|
sutProvider.GetDependency<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>()
|
|
.TryUnprotect(emailVerificationToken, out Arg.Any<RegistrationEmailVerificationTokenable>())
|
|
.Returns(callInfo =>
|
|
{
|
|
callInfo[1] = new RegistrationEmailVerificationTokenable(user.Email, user.Name, receiveMarketingMaterials);
|
|
return true;
|
|
});
|
|
|
|
sutProvider.GetDependency<IUserService>()
|
|
.CreateUserAsync(user, masterPasswordHash)
|
|
.Returns(IdentityResult.Success);
|
|
|
|
// Act
|
|
var result = await sutProvider.Sut.RegisterUserViaEmailVerificationToken(user, masterPasswordHash, emailVerificationToken);
|
|
|
|
// 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 && refEvent.ReceiveMarketingEmails == receiveMarketingMaterials));
|
|
}
|
|
|
|
[Theory]
|
|
[BitAutoData]
|
|
public async Task RegisterUserViaEmailVerificationToken_InvalidToken_ThrowsBadRequestException(SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, string emailVerificationToken, bool receiveMarketingMaterials)
|
|
{
|
|
// Arrange
|
|
sutProvider.GetDependency<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>()
|
|
.TryUnprotect(emailVerificationToken, out Arg.Any<RegistrationEmailVerificationTokenable>())
|
|
.Returns(callInfo =>
|
|
{
|
|
callInfo[1] = new RegistrationEmailVerificationTokenable("wrongEmail@test.com", user.Name, receiveMarketingMaterials);
|
|
return true;
|
|
});
|
|
|
|
// Act & Assert
|
|
var result = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.RegisterUserViaEmailVerificationToken(user, masterPasswordHash, emailVerificationToken));
|
|
Assert.Equal("Invalid email verification token.", result.Message);
|
|
|
|
}
|
|
|
|
}
|