1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-21 12:05:42 +01:00

Auth/PM-12613 - Registration with Email Verification - Provider Invite Flow (#4917)

* PM-12613 - Add RegisterUserViaProviderInviteToken flow (needs manual, unit, and integration tests)

* PM-12613 - RegisterUserCommandTests - test register via provider inv

* PM-12613 - AccountsControllerTests.cs - Add integration test for provider

* PM-12613 - Remove comment

* PM-12613 - Add temp logging to help debug integration test failure in pipeline

* PM-12613 - WebApplicationFactoryBase.cs - add ConfigureServices

* PM-12613 - AccountsControllerTests.cs - refactor test to sidestep encryption

* PM-12613 - Per PR feedback, refactor AccountsController.cs and move token type checking into request model.

* PM-12613 - Remove debug writelines

* PM-12613 - Add RegisterFinishRequestModelTests
This commit is contained in:
Jared Snider 2024-10-23 18:06:24 -04:00 committed by GitHub
parent a952d10637
commit e6245bbece
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 535 additions and 43 deletions

View File

@ -6,6 +6,14 @@ using Bit.Core.Utilities;
namespace Bit.Core.Auth.Models.Api.Request.Accounts;
using System.ComponentModel.DataAnnotations;
public enum RegisterFinishTokenType : byte
{
EmailVerification = 1,
OrganizationInvite = 2,
OrgSponsoredFreeFamilyPlan = 3,
EmergencyAccessInvite = 4,
ProviderInvite = 5,
}
public class RegisterFinishRequestModel : IValidatableObject
{
@ -36,6 +44,10 @@ public class RegisterFinishRequestModel : IValidatableObject
public string? AcceptEmergencyAccessInviteToken { get; set; }
public Guid? AcceptEmergencyAccessId { get; set; }
public string? ProviderInviteToken { get; set; }
public Guid? ProviderUserId { get; set; }
public User ToUser()
{
var user = new User
@ -54,6 +66,32 @@ public class RegisterFinishRequestModel : IValidatableObject
return user;
}
public RegisterFinishTokenType GetTokenType()
{
if (!string.IsNullOrWhiteSpace(EmailVerificationToken))
{
return RegisterFinishTokenType.EmailVerification;
}
if (!string.IsNullOrEmpty(OrgInviteToken) && OrganizationUserId.HasValue)
{
return RegisterFinishTokenType.OrganizationInvite;
}
if (!string.IsNullOrWhiteSpace(OrgSponsoredFreeFamilyPlanToken))
{
return RegisterFinishTokenType.OrgSponsoredFreeFamilyPlan;
}
if (!string.IsNullOrWhiteSpace(AcceptEmergencyAccessInviteToken) && AcceptEmergencyAccessId.HasValue)
{
return RegisterFinishTokenType.EmergencyAccessInvite;
}
if (!string.IsNullOrWhiteSpace(ProviderInviteToken) && ProviderUserId.HasValue)
{
return RegisterFinishTokenType.ProviderInvite;
}
throw new InvalidOperationException("Invalid token type.");
}
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{

View File

@ -61,4 +61,16 @@ public interface IRegisterUserCommand
public Task<IdentityResult> RegisterUserViaAcceptEmergencyAccessInviteToken(User user, string masterPasswordHash,
string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId);
/// <summary>
/// Creates a new user with a given master password hash, sends a welcome email, and raises the signup reference event.
/// If a valid token is provided, the user will be created with their email verified.
/// If the token is invalid or expired, an error will be thrown.
/// </summary>
/// <param name="user">The <see cref="User"/> to create</param>
/// <param name="masterPasswordHash">The hashed master password the user entered</param>
/// <param name="providerInviteToken">The provider invite token sent to the user via email</param>
/// <param name="providerUserId">The provider user id which is used to validate the invite token</param>
/// <returns><see cref="IdentityResult"/></returns>
public Task<IdentityResult> RegisterUserViaProviderInviteToken(User user, string masterPasswordHash, string providerInviteToken, Guid providerUserId);
}

View File

@ -32,6 +32,7 @@ public class RegisterUserCommand : IRegisterUserCommand
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
private readonly IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> _registrationEmailVerificationTokenDataFactory;
private readonly IDataProtector _organizationServiceDataProtector;
private readonly IDataProtector _providerServiceDataProtector;
private readonly ICurrentContext _currentContext;
@ -75,6 +76,8 @@ public class RegisterUserCommand : IRegisterUserCommand
_validateRedemptionTokenCommand = validateRedemptionTokenCommand;
_emergencyAccessInviteTokenDataFactory = emergencyAccessInviteTokenDataFactory;
_providerServiceDataProtector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
}
@ -303,6 +306,25 @@ public class RegisterUserCommand : IRegisterUserCommand
return result;
}
public async Task<IdentityResult> RegisterUserViaProviderInviteToken(User user, string masterPasswordHash,
string providerInviteToken, Guid providerUserId)
{
ValidateOpenRegistrationAllowed();
ValidateProviderInviteToken(providerInviteToken, providerUserId, 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
@ -333,6 +355,15 @@ public class RegisterUserCommand : IRegisterUserCommand
}
}
private void ValidateProviderInviteToken(string providerInviteToken, Guid providerUserId, string userEmail)
{
if (!CoreHelpers.TokenIsValid("ProviderUserInvite", _providerServiceDataProtector, providerInviteToken, userEmail, providerUserId,
_globalSettings.OrganizationInviteExpirationHours))
{
throw new BadRequestException("Invalid provider invite token.");
}
}
private RegistrationEmailVerificationTokenable ValidateRegistrationEmailVerificationTokenable(string emailVerificationToken, string userEmail)
{

View File

@ -1,4 +1,5 @@
using Bit.Core;
using System.Diagnostics;
using Bit.Core;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Auth.Models.Api.Response.Accounts;
@ -149,40 +150,44 @@ public class AccountsController : Controller
IdentityResult identityResult = null;
var delaysEnabled = !_featureService.IsEnabled(FeatureFlagKeys.EmailVerificationDisableTimingDelays);
if (!string.IsNullOrEmpty(model.OrgInviteToken) && model.OrganizationUserId.HasValue)
switch (model.GetTokenType())
{
identityResult = await _registerUserCommand.RegisterUserViaOrganizationInviteToken(user, model.MasterPasswordHash,
model.OrgInviteToken, model.OrganizationUserId);
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);
}
if (!string.IsNullOrEmpty(model.AcceptEmergencyAccessInviteToken) && model.AcceptEmergencyAccessId.HasValue)
{
identityResult = await _registerUserCommand.RegisterUserViaAcceptEmergencyAccessInviteToken(user, model.MasterPasswordHash,
model.AcceptEmergencyAccessInviteToken, model.AcceptEmergencyAccessId.Value);
return await ProcessRegistrationResult(identityResult, user, delaysEnabled);
}
if (string.IsNullOrEmpty(model.EmailVerificationToken))
{
throw new BadRequestException("Invalid registration finish request");
}
case RegisterFinishTokenType.EmailVerification:
identityResult =
await _registerUserCommand.RegisterUserViaEmailVerificationToken(user, model.MasterPasswordHash,
model.EmailVerificationToken);
return await ProcessRegistrationResult(identityResult, user, delaysEnabled);
break;
case RegisterFinishTokenType.OrganizationInvite:
identityResult = await _registerUserCommand.RegisterUserViaOrganizationInviteToken(user, model.MasterPasswordHash,
model.OrgInviteToken, model.OrganizationUserId);
return await ProcessRegistrationResult(identityResult, user, delaysEnabled);
break;
case RegisterFinishTokenType.OrgSponsoredFreeFamilyPlan:
identityResult = await _registerUserCommand.RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(user, model.MasterPasswordHash, model.OrgSponsoredFreeFamilyPlanToken);
return await ProcessRegistrationResult(identityResult, user, delaysEnabled);
break;
case RegisterFinishTokenType.EmergencyAccessInvite:
Debug.Assert(model.AcceptEmergencyAccessId.HasValue);
identityResult = await _registerUserCommand.RegisterUserViaAcceptEmergencyAccessInviteToken(user, model.MasterPasswordHash,
model.AcceptEmergencyAccessInviteToken, model.AcceptEmergencyAccessId.Value);
return await ProcessRegistrationResult(identityResult, user, delaysEnabled);
break;
case RegisterFinishTokenType.ProviderInvite:
Debug.Assert(model.ProviderUserId.HasValue);
identityResult = await _registerUserCommand.RegisterUserViaProviderInviteToken(user, model.MasterPasswordHash,
model.ProviderInviteToken, model.ProviderUserId.Value);
return await ProcessRegistrationResult(identityResult, user, delaysEnabled);
break;
default:
throw new BadRequestException("Invalid registration finish request");
}
}
private async Task<RegisterResponseModel> ProcessRegistrationResult(IdentityResult result, User user, bool delaysEnabled)

View File

@ -0,0 +1,173 @@
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Enums;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Core.Test.Auth.Models.Api.Request.Accounts;
public class RegisterFinishRequestModelTests
{
[Theory]
[BitAutoData]
public void GetTokenType_Returns_EmailVerification(string email, string masterPasswordHash,
string userSymmetricKey, KeysRequestModel userAsymmetricKeys, KdfType kdf, int kdfIterations, string emailVerificationToken)
{
// Arrange
var model = new RegisterFinishRequestModel
{
Email = email,
MasterPasswordHash = masterPasswordHash,
UserSymmetricKey = userSymmetricKey,
UserAsymmetricKeys = userAsymmetricKeys,
Kdf = kdf,
KdfIterations = kdfIterations,
EmailVerificationToken = emailVerificationToken
};
// Act
Assert.Equal(RegisterFinishTokenType.EmailVerification, model.GetTokenType());
}
[Theory]
[BitAutoData]
public void GetTokenType_Returns_OrganizationInvite(string email, string masterPasswordHash,
string userSymmetricKey, KeysRequestModel userAsymmetricKeys, KdfType kdf, int kdfIterations, string orgInviteToken, Guid organizationUserId)
{
// Arrange
var model = new RegisterFinishRequestModel
{
Email = email,
MasterPasswordHash = masterPasswordHash,
UserSymmetricKey = userSymmetricKey,
UserAsymmetricKeys = userAsymmetricKeys,
Kdf = kdf,
KdfIterations = kdfIterations,
OrgInviteToken = orgInviteToken,
OrganizationUserId = organizationUserId
};
// Act
Assert.Equal(RegisterFinishTokenType.OrganizationInvite, model.GetTokenType());
}
[Theory]
[BitAutoData]
public void GetTokenType_Returns_OrgSponsoredFreeFamilyPlan(string email, string masterPasswordHash,
string userSymmetricKey, KeysRequestModel userAsymmetricKeys, KdfType kdf, int kdfIterations, string orgSponsoredFreeFamilyPlanToken)
{
// Arrange
var model = new RegisterFinishRequestModel
{
Email = email,
MasterPasswordHash = masterPasswordHash,
UserSymmetricKey = userSymmetricKey,
UserAsymmetricKeys = userAsymmetricKeys,
Kdf = kdf,
KdfIterations = kdfIterations,
OrgSponsoredFreeFamilyPlanToken = orgSponsoredFreeFamilyPlanToken
};
// Act
Assert.Equal(RegisterFinishTokenType.OrgSponsoredFreeFamilyPlan, model.GetTokenType());
}
[Theory]
[BitAutoData]
public void GetTokenType_Returns_EmergencyAccessInvite(string email, string masterPasswordHash,
string userSymmetricKey, KeysRequestModel userAsymmetricKeys, KdfType kdf, int kdfIterations, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId)
{
// Arrange
var model = new RegisterFinishRequestModel
{
Email = email,
MasterPasswordHash = masterPasswordHash,
UserSymmetricKey = userSymmetricKey,
UserAsymmetricKeys = userAsymmetricKeys,
Kdf = kdf,
KdfIterations = kdfIterations,
AcceptEmergencyAccessInviteToken = acceptEmergencyAccessInviteToken,
AcceptEmergencyAccessId = acceptEmergencyAccessId
};
// Act
Assert.Equal(RegisterFinishTokenType.EmergencyAccessInvite, model.GetTokenType());
}
[Theory]
[BitAutoData]
public void GetTokenType_Returns_ProviderInvite(string email, string masterPasswordHash,
string userSymmetricKey, KeysRequestModel userAsymmetricKeys, KdfType kdf, int kdfIterations, string providerInviteToken, Guid providerUserId)
{
// Arrange
var model = new RegisterFinishRequestModel
{
Email = email,
MasterPasswordHash = masterPasswordHash,
UserSymmetricKey = userSymmetricKey,
UserAsymmetricKeys = userAsymmetricKeys,
Kdf = kdf,
KdfIterations = kdfIterations,
ProviderInviteToken = providerInviteToken,
ProviderUserId = providerUserId
};
// Act
Assert.Equal(RegisterFinishTokenType.ProviderInvite, model.GetTokenType());
}
[Theory]
[BitAutoData]
public void GetTokenType_Returns_Invalid(string email, string masterPasswordHash,
string userSymmetricKey, KeysRequestModel userAsymmetricKeys, KdfType kdf, int kdfIterations)
{
// Arrange
var model = new RegisterFinishRequestModel
{
Email = email,
MasterPasswordHash = masterPasswordHash,
UserSymmetricKey = userSymmetricKey,
UserAsymmetricKeys = userAsymmetricKeys,
Kdf = kdf,
KdfIterations = kdfIterations
};
// Act
var result = Assert.Throws<InvalidOperationException>(() => model.GetTokenType());
Assert.Equal("Invalid token type.", result.Message);
}
[Theory]
[BitAutoData]
public void ToUser_Returns_User(string email, string masterPasswordHash, string masterPasswordHint,
string userSymmetricKey, KeysRequestModel userAsymmetricKeys, KdfType kdf, int kdfIterations,
int? kdfMemory, int? kdfParallelism)
{
// Arrange
var model = new RegisterFinishRequestModel
{
Email = email,
MasterPasswordHash = masterPasswordHash,
MasterPasswordHint = masterPasswordHint,
UserSymmetricKey = userSymmetricKey,
UserAsymmetricKeys = userAsymmetricKeys,
Kdf = kdf,
KdfIterations = kdfIterations,
KdfMemory = kdfMemory,
KdfParallelism = kdfParallelism
};
// Act
var result = model.ToUser();
// Assert
Assert.Equal(email, result.Email);
Assert.Equal(masterPasswordHint, result.MasterPasswordHint);
Assert.Equal(kdf, result.Kdf);
Assert.Equal(kdfIterations, result.KdfIterations);
Assert.Equal(kdfMemory, result.KdfMemory);
Assert.Equal(kdfParallelism, result.KdfParallelism);
Assert.Equal(userSymmetricKey, result.Key);
Assert.Equal(userAsymmetricKeys.PublicKey, result.PublicKey);
Assert.Equal(userAsymmetricKeys.EncryptedPrivateKey, result.PrivateKey);
}
}

View File

@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Entities;
using System.Text;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Entities;
@ -19,7 +20,9 @@ using Bit.Core.Tools.Services;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.WebUtilities;
using NSubstitute;
using Xunit;
@ -28,8 +31,10 @@ 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)
@ -86,7 +91,10 @@ public class RegisterUserCommandTests
.RaiseEventAsync(Arg.Any<ReferenceEvent>());
}
// -----------------------------------------------------------------------------------------------
// RegisterUserWithOrganizationInviteToken tests
// -----------------------------------------------------------------------------------------------
// Simple happy path test
[Theory]
[BitAutoData]
@ -312,7 +320,10 @@ public class RegisterUserCommandTests
Assert.Equal(expectedErrorMessage, exception.Message);
}
// RegisterUserViaEmailVerificationToken
// -----------------------------------------------------------------------------------------------
// RegisterUserViaEmailVerificationToken tests
// -----------------------------------------------------------------------------------------------
[Theory]
[BitAutoData]
public async Task RegisterUserViaEmailVerificationToken_Succeeds(SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, string emailVerificationToken, bool receiveMarketingMaterials)
@ -382,10 +393,9 @@ public class RegisterUserCommandTests
}
// RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken
// -----------------------------------------------------------------------------------------------
// RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken tests
// -----------------------------------------------------------------------------------------------
[Theory]
[BitAutoData]
@ -452,7 +462,9 @@ public class RegisterUserCommandTests
Assert.Equal("Open registration has been disabled by the system administrator.", result.Message);
}
// RegisterUserViaAcceptEmergencyAccessInviteToken
// -----------------------------------------------------------------------------------------------
// RegisterUserViaAcceptEmergencyAccessInviteToken tests
// -----------------------------------------------------------------------------------------------
[Theory]
[BitAutoData]
@ -495,8 +507,6 @@ public class RegisterUserCommandTests
.RaiseEventAsync(Arg.Is<ReferenceEvent>(refEvent => refEvent.Type == ReferenceEventType.Signup));
}
[Theory]
[BitAutoData]
public async Task RegisterUserViaAcceptEmergencyAccessInviteToken_InvalidToken_ThrowsBadRequestException(SutProvider<RegisterUserCommand> sutProvider, User user,
@ -536,5 +546,140 @@ public class RegisterUserCommandTests
Assert.Equal("Open registration has been disabled by the system administrator.", result.Message);
}
// -----------------------------------------------------------------------------------------------
// RegisterUserViaProviderInviteToken tests
// -----------------------------------------------------------------------------------------------
[Theory]
[BitAutoData]
public async Task RegisterUserViaProviderInviteToken_Succeeds(SutProvider<RegisterUserCommand> sutProvider,
User user, string masterPasswordHash, Guid providerUserId)
{
// Arrange
// Start with plaintext
var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);
var decryptedProviderInviteToken = $"ProviderUserInvite {providerUserId} {user.Email} {nowMillis}";
// Get the byte array of the plaintext
var decryptedProviderInviteTokenByteArray = Encoding.UTF8.GetBytes(decryptedProviderInviteToken);
// Base64 encode the byte array (this is passed to protector.protect(bytes))
var base64EncodedProviderInvToken = WebEncoders.Base64UrlEncode(decryptedProviderInviteTokenByteArray);
var mockDataProtector = Substitute.For<IDataProtector>();
// Given any byte array, just return the decryptedProviderInviteTokenByteArray (sidestepping any actual encryption)
mockDataProtector.Unprotect(Arg.Any<byte[]>()).Returns(decryptedProviderInviteTokenByteArray);
sutProvider.GetDependency<IDataProtectionProvider>()
.CreateProtector("ProviderServiceDataProtector")
.Returns(mockDataProtector);
sutProvider.GetDependency<IGlobalSettings>()
.OrganizationInviteExpirationHours.Returns(120); // 5 days
sutProvider.GetDependency<IUserService>()
.CreateUserAsync(user, masterPasswordHash)
.Returns(IdentityResult.Success);
// Using sutProvider in the parameters of the function means that the constructor has already run for the
// command so we have to recreate it in order for our mock overrides to be used.
sutProvider.Create();
// Act
var result = await sutProvider.Sut.RegisterUserViaProviderInviteToken(user, masterPasswordHash, base64EncodedProviderInvToken, providerUserId);
// 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 RegisterUserViaProviderInviteToken_InvalidToken_ThrowsBadRequestException(SutProvider<RegisterUserCommand> sutProvider,
User user, string masterPasswordHash, Guid providerUserId)
{
// Arrange
// Start with plaintext
var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);
var decryptedProviderInviteToken = $"ProviderUserInvite {providerUserId} {user.Email} {nowMillis}";
// Get the byte array of the plaintext
var decryptedProviderInviteTokenByteArray = Encoding.UTF8.GetBytes(decryptedProviderInviteToken);
// Base64 encode the byte array (this is passed to protector.protect(bytes))
var base64EncodedProviderInvToken = WebEncoders.Base64UrlEncode(decryptedProviderInviteTokenByteArray);
var mockDataProtector = Substitute.For<IDataProtector>();
// Given any byte array, just return the decryptedProviderInviteTokenByteArray (sidestepping any actual encryption)
mockDataProtector.Unprotect(Arg.Any<byte[]>()).Returns(decryptedProviderInviteTokenByteArray);
sutProvider.GetDependency<IDataProtectionProvider>()
.CreateProtector("ProviderServiceDataProtector")
.Returns(mockDataProtector);
sutProvider.GetDependency<IGlobalSettings>()
.OrganizationInviteExpirationHours.Returns(120); // 5 days
// Using sutProvider in the parameters of the function means that the constructor has already run for the
// command so we have to recreate it in order for our mock overrides to be used.
sutProvider.Create();
// Act & Assert
var result = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.RegisterUserViaProviderInviteToken(user, masterPasswordHash, base64EncodedProviderInvToken, Guid.NewGuid()));
Assert.Equal("Invalid provider invite token.", result.Message);
}
[Theory]
[BitAutoData]
public async Task RegisterUserViaProviderInviteToken_DisabledOpenRegistration_ThrowsBadRequestException(SutProvider<RegisterUserCommand> sutProvider,
User user, string masterPasswordHash, Guid providerUserId)
{
// Arrange
// Start with plaintext
var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);
var decryptedProviderInviteToken = $"ProviderUserInvite {providerUserId} {user.Email} {nowMillis}";
// Get the byte array of the plaintext
var decryptedProviderInviteTokenByteArray = Encoding.UTF8.GetBytes(decryptedProviderInviteToken);
// Base64 encode the byte array (this is passed to protector.protect(bytes))
var base64EncodedProviderInvToken = WebEncoders.Base64UrlEncode(decryptedProviderInviteTokenByteArray);
var mockDataProtector = Substitute.For<IDataProtector>();
// Given any byte array, just return the decryptedProviderInviteTokenByteArray (sidestepping any actual encryption)
mockDataProtector.Unprotect(Arg.Any<byte[]>()).Returns(decryptedProviderInviteTokenByteArray);
sutProvider.GetDependency<IDataProtectionProvider>()
.CreateProtector("ProviderServiceDataProtector")
.Returns(mockDataProtector);
sutProvider.GetDependency<IGlobalSettings>()
.DisableUserRegistration = true;
// Using sutProvider in the parameters of the function means that the constructor has already run for the
// command so we have to recreate it in order for our mock overrides to be used.
sutProvider.Create();
// Act & Assert
var result = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.RegisterUserViaProviderInviteToken(user, masterPasswordHash, base64EncodedProviderInvToken, providerUserId));
Assert.Equal("Open registration has been disabled by the system administrator.", result.Message);
}
}

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using System.Text;
using Bit.Core;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Models.Api.Request.Accounts;
@ -9,10 +10,12 @@ 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;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.EntityFrameworkCore;
using NSubstitute;
using Xunit;
@ -470,6 +473,80 @@ public class AccountsControllerTests : IClassFixture<IdentityApplicationFactory>
Assert.Equal(kdfParallelism, user.KdfParallelism);
}
[Theory, BitAutoData]
public async Task RegistrationWithEmailVerification_WithProviderInviteToken_Succeeds(
[StringLength(1000)] string masterPasswordHash, [StringLength(50)] string masterPasswordHint, string userSymmetricKey,
KeysRequestModel userAsymmetricKeys, int kdfMemory, int kdfParallelism)
{
// Localize factory to just this test.
var localFactory = new IdentityApplicationFactory();
// Hardcoded, valid data
var email = "jsnider+local253@bitwarden.com";
var providerUserId = new Guid("c6fdba35-2e52-43b4-8fb7-b211011d154a");
var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);
var decryptedProviderInviteToken = $"ProviderUserInvite {providerUserId} {email} {nowMillis}";
// var providerInviteToken = await GetValidProviderInviteToken(localFactory, email, providerUserId);
// Get the byte array of the plaintext
var decryptedProviderInviteTokenByteArray = Encoding.UTF8.GetBytes(decryptedProviderInviteToken);
// Base64 encode the byte array (this is passed to protector.protect(bytes))
var base64EncodedProviderInvToken = WebEncoders.Base64UrlEncode(decryptedProviderInviteTokenByteArray);
var mockDataProtector = Substitute.For<IDataProtector>();
mockDataProtector.Unprotect(Arg.Any<byte[]>()).Returns(decryptedProviderInviteTokenByteArray);
localFactory.SubstituteService<IDataProtectionProvider>(dataProtectionProvider =>
{
dataProtectionProvider.CreateProtector(Arg.Any<string>())
.Returns(mockDataProtector);
});
// As token contains now milliseconds for when it was created, create 1k year timespan for expiration
// to ensure token is valid for a good long while.
localFactory.UpdateConfiguration("globalSettings:OrganizationInviteExpirationHours", "8760000");
var registerFinishReqModel = new RegisterFinishRequestModel
{
Email = email,
MasterPasswordHash = masterPasswordHash,
MasterPasswordHint = masterPasswordHint,
ProviderInviteToken = base64EncodedProviderInvToken,
ProviderUserId = providerUserId,
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(
@ -527,4 +604,5 @@ public class AccountsControllerTests : IClassFixture<IdentityApplicationFactory>
return user;
}
}

View File

@ -57,6 +57,16 @@ public abstract class WebApplicationFactoryBase<T> : WebApplicationFactory<T>
});
}
/// <summary>
/// Allows you to add your own services to the application as required.
/// </summary>
/// <param name="configure">The service collection you want added to the test service collection.</param>
/// <remarks>This needs to be ran BEFORE making any calls through the factory to take effect.</remarks>
public void ConfigureServices(Action<IServiceCollection> configure)
{
_configureTestServices.Add(configure);
}
/// <summary>
/// Add your own configuration provider to the application.
/// </summary>