Revert "feat(register): [PM-27084] Account Register Uses New Data Types (#6715)" (#6854)

This reverts commit 8cb8030534.
This commit is contained in:
Patrick-Pimentel-Bitwarden
2026-01-15 16:19:16 -05:00
committed by GitHub
parent 8cb8030534
commit 029a5f6a2d
19 changed files with 63 additions and 1045 deletions

View File

@@ -3,6 +3,7 @@ using System.Text.Json;
using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.IntegrationTest.Factories;
using Bit.Api.IntegrationTest.Helpers;
using Bit.Api.KeyManagement.Models.Requests;
using Bit.Api.Models.Response;
using Bit.Core;
using Bit.Core.Auth.Entities;
@@ -11,7 +12,6 @@ using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Repositories;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Models.Api.Request;
using Bit.Core.KeyManagement.Repositories;
using Bit.Core.Models.Data;
using Bit.Core.Platform.Push;
@@ -378,7 +378,7 @@ public class AccountsControllerTest : IClassFixture<ApiApplicationFactory>, IAsy
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var content = await response.Content.ReadAsStringAsync();
Assert.Contains("The model state is invalid", content);
Assert.Contains("KDF settings are invalid", content);
}
[Fact]

View File

@@ -1,6 +1,7 @@
using System.Security.Claims;
using Bit.Api.Auth.Controllers;
using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.KeyManagement.Models.Requests;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Models.Api.Request.Accounts;

View File

@@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations;
using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.KeyManagement.Models.Requests;
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Entities;
using Bit.Core.Enums;

View File

@@ -29,9 +29,7 @@ internal class RegisterFinishRequestModelCustomization : ICustomization
.With(o => o.OrgInviteToken, OrgInviteToken)
.With(o => o.OrgSponsoredFreeFamilyPlanToken, OrgSponsoredFreeFamilyPlanToken)
.With(o => o.AcceptEmergencyAccessInviteToken, AcceptEmergencyAccessInviteToken)
.With(o => o.ProviderInviteToken, ProviderInviteToken)
.Without(o => o.MasterPasswordAuthentication)
.Without(o => o.MasterPasswordUnlock));
.With(o => o.ProviderInviteToken, ProviderInviteToken));
}
}

View File

@@ -1,6 +1,5 @@
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Models.Api.Request;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
@@ -8,17 +7,6 @@ namespace Bit.Core.Test.Auth.Models.Api.Request.Accounts;
public class RegisterFinishRequestModelTests
{
private static List<System.ComponentModel.DataAnnotations.ValidationResult> Validate(RegisterFinishRequestModel model)
{
var results = new List<System.ComponentModel.DataAnnotations.ValidationResult>();
System.ComponentModel.DataAnnotations.Validator.TryValidateObject(
model,
new System.ComponentModel.DataAnnotations.ValidationContext(model),
results,
true);
return results;
}
[Theory]
[BitAutoData]
public void GetTokenType_Returns_EmailVerification(string email, string masterPasswordHash,
@@ -182,175 +170,4 @@ public class RegisterFinishRequestModelTests
Assert.Equal(userAsymmetricKeys.PublicKey, result.PublicKey);
Assert.Equal(userAsymmetricKeys.EncryptedPrivateKey, result.PrivateKey);
}
[Fact]
public void Validate_WhenBothAuthAndRootHashProvidedButNotEqual_ReturnsMismatchError()
{
var model = new RegisterFinishRequestModel
{
Email = "user@example.com",
MasterPasswordHash = "root-hash",
UserAsymmetricKeys = new KeysRequestModel { PublicKey = "pk", EncryptedPrivateKey = "sk" },
// Provide both unlock and authentication with valid KDF so only the mismatch rule fires
MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel
{
Kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = AuthConstants.PBKDF2_ITERATIONS.Default },
MasterKeyWrappedUserKey = "wrapped",
Salt = "salt"
},
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
{
Kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = AuthConstants.PBKDF2_ITERATIONS.Default },
MasterPasswordAuthenticationHash = "auth-hash", // different than root
Salt = "salt"
},
// Provide any valid token so we don't fail token validation
EmailVerificationToken = "token"
};
var results = Validate(model);
Assert.Contains(results, r =>
r.ErrorMessage == $"{nameof(MasterPasswordAuthenticationDataRequestModel.MasterPasswordAuthenticationHash)} and root level {nameof(RegisterFinishRequestModel.MasterPasswordHash)} provided and are not equal. Only provide one.");
}
[Fact]
public void Validate_WhenAuthProvidedButUnlockMissing_ReturnsUnlockMissingError()
{
var model = new RegisterFinishRequestModel
{
Email = "user@example.com",
UserAsymmetricKeys = new KeysRequestModel { PublicKey = "pk", EncryptedPrivateKey = "sk" },
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
{
Kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = AuthConstants.PBKDF2_ITERATIONS.Default },
MasterPasswordAuthenticationHash = "auth-hash",
Salt = "salt"
},
EmailVerificationToken = "token"
};
var results = Validate(model);
Assert.Contains(results, r => r.ErrorMessage == "MasterPasswordUnlock not found on RequestModel");
}
[Fact]
public void Validate_WhenUnlockProvidedButAuthMissing_ReturnsAuthMissingError()
{
var model = new RegisterFinishRequestModel
{
Email = "user@example.com",
UserAsymmetricKeys = new KeysRequestModel { PublicKey = "pk", EncryptedPrivateKey = "sk" },
MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel
{
Kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = AuthConstants.PBKDF2_ITERATIONS.Default },
MasterKeyWrappedUserKey = "wrapped",
Salt = "salt"
},
EmailVerificationToken = "token"
};
var results = Validate(model);
Assert.Contains(results, r => r.ErrorMessage == "MasterPasswordAuthentication not found on RequestModel");
}
[Fact]
public void Validate_WhenNeitherAuthNorUnlock_AndRootKdfMissing_ReturnsBothRootKdfErrors()
{
var model = new RegisterFinishRequestModel
{
Email = "user@example.com",
UserAsymmetricKeys = new KeysRequestModel { PublicKey = "pk", EncryptedPrivateKey = "sk" },
// No MasterPasswordUnlock, no MasterPasswordAuthentication
// No root Kdf and KdfIterations to trigger both errors
EmailVerificationToken = "token"
};
var results = Validate(model);
Assert.Contains(results, r => r.ErrorMessage == $"{nameof(RegisterFinishRequestModel.Kdf)} not found on RequestModel");
Assert.Contains(results, r => r.ErrorMessage == $"{nameof(RegisterFinishRequestModel.KdfIterations)} not found on RequestModel");
}
[Fact]
public void Validate_WhenAuthAndRootHashBothMissing_ReturnsMissingHashErrorOnly()
{
var model = new RegisterFinishRequestModel
{
Email = "user@example.com",
UserAsymmetricKeys = new KeysRequestModel { PublicKey = "pk", EncryptedPrivateKey = "sk" },
// Both MasterPasswordAuthentication and MasterPasswordHash are missing
MasterPasswordAuthentication = null,
MasterPasswordHash = null,
// Provide valid root KDF to avoid root KDF errors
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
EmailVerificationToken = "token" // avoid token error
};
var results = Validate(model);
// Only the new missing hash error should be present
Assert.Single(results);
Assert.Equal($"{nameof(MasterPasswordAuthenticationDataRequestModel.MasterPasswordAuthenticationHash)} and {nameof(RegisterFinishRequestModel.MasterPasswordHash)} not found on request, one needs to be defined.", results[0].ErrorMessage);
Assert.Contains(nameof(MasterPasswordAuthenticationDataRequestModel.MasterPasswordAuthenticationHash), results[0].MemberNames);
Assert.Contains(nameof(RegisterFinishRequestModel.MasterPasswordHash), results[0].MemberNames);
}
[Fact]
public void Validate_WhenAllFieldsValidWithSubModels_IsValid()
{
var model = new RegisterFinishRequestModel
{
Email = "user@example.com",
UserAsymmetricKeys = new KeysRequestModel { PublicKey = "pk", EncryptedPrivateKey = "sk" },
MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel
{
Kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = AuthConstants.PBKDF2_ITERATIONS.Default },
MasterKeyWrappedUserKey = "wrapped",
Salt = "salt"
},
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
{
Kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = AuthConstants.PBKDF2_ITERATIONS.Default },
MasterPasswordAuthenticationHash = "auth-hash",
Salt = "salt"
},
EmailVerificationToken = "token"
};
var results = Validate(model);
Assert.Empty(results);
}
[Fact]
public void Validate_WhenNoValidRegistrationTokenProvided_ReturnsTokenErrorOnly()
{
var model = new RegisterFinishRequestModel
{
Email = "user@example.com",
UserAsymmetricKeys = new KeysRequestModel { PublicKey = "pk", EncryptedPrivateKey = "sk" },
MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel
{
Kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = AuthConstants.PBKDF2_ITERATIONS.Default },
MasterKeyWrappedUserKey = "wrapped",
Salt = "salt"
},
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
{
Kdf = new KdfRequestModel { KdfType = KdfType.PBKDF2_SHA256, Iterations = AuthConstants.PBKDF2_ITERATIONS.Default },
MasterPasswordAuthenticationHash = "auth-hash",
Salt = "salt"
}
// No token fields set
};
var results = Validate(model);
Assert.Single(results);
Assert.Equal("No valid registration token provided", results[0].ErrorMessage);
}
}

View File

@@ -1,5 +1,4 @@
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using System.Reflection;
using System.Text;
using Bit.Core;
using Bit.Core.Auth.Models.Api.Request.Accounts;
@@ -10,7 +9,6 @@ using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Models.Api.Request;
using Bit.Core.Models.Data;
using Bit.Core.Repositories;
using Bit.Core.Services;
@@ -592,504 +590,6 @@ public class AccountsControllerTests : IDisposable
await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostRegisterVerificationEmailClicked(requestModel));
}
// PM-28143 - When removing the old properties, update this test to just test the new properties working
// as expected.
[Theory, BitAutoData]
public async Task PostRegisterFinish_EmailVerification_BothDataForms_ProduceEquivalentOutcomes(
string email,
string emailVerificationToken,
string masterPasswordHash,
string masterKeyWrappedUserKey,
string publicKey,
string encryptedPrivateKey)
{
// Arrange: new-form model (MasterPasswordAuthenticationData + MasterPasswordUnlockData)
var kdfData = new KdfRequestModel
{
KdfType = KdfType.Argon2id,
Iterations = AuthConstants.ARGON2_ITERATIONS.Default,
Memory = AuthConstants.ARGON2_MEMORY.Default,
Parallelism = AuthConstants.ARGON2_PARALLELISM.Default
};
var newModel = new RegisterFinishRequestModel
{
Email = email,
EmailVerificationToken = emailVerificationToken,
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
{
Kdf = kdfData,
MasterPasswordAuthenticationHash = masterPasswordHash,
Salt = email // salt choice is not validated here during registration
},
MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel
{
Kdf = kdfData,
MasterKeyWrappedUserKey = masterKeyWrappedUserKey,
Salt = email
},
UserAsymmetricKeys = new KeysRequestModel
{
PublicKey = publicKey,
EncryptedPrivateKey = encryptedPrivateKey
}
};
// Arrange: legacy-form model (MasterPasswordHash + legacy KDF + UserSymmetricKey)
var legacyModel = new RegisterFinishRequestModel
{
Email = email,
EmailVerificationToken = emailVerificationToken,
MasterPasswordHash = masterPasswordHash,
Kdf = KdfType.Argon2id,
KdfIterations = AuthConstants.ARGON2_ITERATIONS.Default,
KdfMemory = AuthConstants.ARGON2_MEMORY.Default,
KdfParallelism = AuthConstants.ARGON2_PARALLELISM.Default,
UserSymmetricKey = masterKeyWrappedUserKey,
UserAsymmetricKeys = new KeysRequestModel
{
PublicKey = publicKey,
EncryptedPrivateKey = encryptedPrivateKey
}
};
var newUser = newModel.ToUser();
var legacyUser = legacyModel.ToUser();
_registerUserCommand
.RegisterUserViaEmailVerificationToken(Arg.Any<User>(), masterPasswordHash, emailVerificationToken)
.Returns(Task.FromResult(IdentityResult.Success));
// Act: call with new form
var newResult = await _sut.PostRegisterFinish(newModel);
// Act: call with legacy form
var legacyResult = await _sut.PostRegisterFinish(legacyModel);
// Assert: outcomes are identical in effect (success response)
Assert.NotNull(newResult);
Assert.NotNull(legacyResult);
// Assert: effective users are equivalent
Assert.Equal(legacyUser.Email, newUser.Email);
Assert.Equal(legacyUser.MasterPasswordHint, newUser.MasterPasswordHint);
Assert.Equal(legacyUser.Kdf, newUser.Kdf);
Assert.Equal(legacyUser.KdfIterations, newUser.KdfIterations);
Assert.Equal(legacyUser.KdfMemory, newUser.KdfMemory);
Assert.Equal(legacyUser.KdfParallelism, newUser.KdfParallelism);
Assert.Equal(legacyUser.Key, newUser.Key);
Assert.Equal(legacyUser.PublicKey, newUser.PublicKey);
Assert.Equal(legacyUser.PrivateKey, newUser.PrivateKey);
// Assert: hash forwarded identically from both inputs
await _registerUserCommand.Received(2).RegisterUserViaEmailVerificationToken(
Arg.Is<User>(u =>
u.Email == newUser.Email &&
u.Kdf == newUser.Kdf &&
u.KdfIterations == newUser.KdfIterations &&
u.KdfMemory == newUser.KdfMemory &&
u.KdfParallelism == newUser.KdfParallelism &&
u.Key == newUser.Key),
masterPasswordHash,
emailVerificationToken);
await _registerUserCommand.Received(2).RegisterUserViaEmailVerificationToken(
Arg.Is<User>(u =>
u.Email == legacyUser.Email &&
u.Kdf == legacyUser.Kdf &&
u.KdfIterations == legacyUser.KdfIterations &&
u.KdfMemory == legacyUser.KdfMemory &&
u.KdfParallelism == legacyUser.KdfParallelism &&
u.Key == legacyUser.Key),
masterPasswordHash,
emailVerificationToken);
}
// PM-28143 - When removing the old properties, update this test to just test the new properties working
// as expected.
[Theory, BitAutoData]
public async Task PostRegisterFinish_OrgInvite_BothDataForms_ProduceEquivalentOutcomes(
string email,
string orgInviteToken,
Guid organizationUserId,
string masterPasswordHash,
string masterKeyWrappedUserKey,
string publicKey,
string encryptedPrivateKey)
{
var kdfData = new KdfRequestModel
{
KdfType = KdfType.Argon2id,
Iterations = AuthConstants.ARGON2_ITERATIONS.Default,
Memory = AuthConstants.ARGON2_MEMORY.Default,
Parallelism = AuthConstants.ARGON2_PARALLELISM.Default
};
// Arrange: new-form model (MasterPasswordAuthenticationData + MasterPasswordUnlockData)
var newModel = new RegisterFinishRequestModel
{
Email = email,
OrgInviteToken = orgInviteToken,
OrganizationUserId = organizationUserId,
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
{
Kdf = kdfData,
MasterPasswordAuthenticationHash = masterPasswordHash,
Salt = email
},
MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel
{
Kdf = kdfData,
MasterKeyWrappedUserKey = masterKeyWrappedUserKey,
Salt = email
},
UserAsymmetricKeys = new KeysRequestModel
{
PublicKey = publicKey,
EncryptedPrivateKey = encryptedPrivateKey
}
};
// Arrange: legacy-form model (MasterPasswordHash + legacy KDF + UserSymmetricKey)
var legacyModel = new RegisterFinishRequestModel
{
Email = email,
OrgInviteToken = orgInviteToken,
OrganizationUserId = organizationUserId,
MasterPasswordHash = masterPasswordHash,
Kdf = kdfData.KdfType,
KdfIterations = kdfData.Iterations,
KdfMemory = kdfData.Memory,
KdfParallelism = kdfData.Parallelism,
UserSymmetricKey = masterKeyWrappedUserKey,
UserAsymmetricKeys = new KeysRequestModel
{
PublicKey = publicKey,
EncryptedPrivateKey = encryptedPrivateKey
}
};
var newUser = newModel.ToUser();
var legacyUser = legacyModel.ToUser();
_registerUserCommand
.RegisterUserViaOrganizationInviteToken(Arg.Any<User>(), masterPasswordHash, orgInviteToken, organizationUserId)
.Returns(Task.FromResult(IdentityResult.Success));
// Act
var newResult = await _sut.PostRegisterFinish(newModel);
var legacyResult = await _sut.PostRegisterFinish(legacyModel);
// Assert success
Assert.NotNull(newResult);
Assert.NotNull(legacyResult);
// Assert: effective users are equivalent
Assert.Equal(legacyUser.Email, newUser.Email);
Assert.Equal(legacyUser.MasterPasswordHint, newUser.MasterPasswordHint);
Assert.Equal(legacyUser.Kdf, newUser.Kdf);
Assert.Equal(legacyUser.KdfIterations, newUser.KdfIterations);
Assert.Equal(legacyUser.KdfMemory, newUser.KdfMemory);
Assert.Equal(legacyUser.KdfParallelism, newUser.KdfParallelism);
Assert.Equal(legacyUser.Key, newUser.Key);
Assert.Equal(legacyUser.PublicKey, newUser.PublicKey);
Assert.Equal(legacyUser.PrivateKey, newUser.PrivateKey);
// Assert: hash forwarded identically from both inputs
await _registerUserCommand.Received(2).RegisterUserViaOrganizationInviteToken(
Arg.Is<User>(u =>
u.Email == newUser.Email &&
u.Kdf == newUser.Kdf &&
u.KdfIterations == newUser.KdfIterations &&
u.KdfMemory == newUser.KdfMemory &&
u.KdfParallelism == newUser.KdfParallelism &&
u.Key == newUser.Key),
masterPasswordHash,
orgInviteToken,
organizationUserId);
await _registerUserCommand.Received(2).RegisterUserViaOrganizationInviteToken(
Arg.Is<User>(u =>
u.Email == legacyUser.Email &&
u.Kdf == legacyUser.Kdf &&
u.KdfIterations == legacyUser.KdfIterations &&
u.KdfMemory == legacyUser.KdfMemory &&
u.KdfParallelism == legacyUser.KdfParallelism &&
u.Key == legacyUser.Key),
masterPasswordHash,
orgInviteToken,
organizationUserId);
}
[Theory, BitAutoData]
public async Task PostRegisterFinish_NewForm_UsesUnlockDataForKdfAndKey_WhenRootFieldsNull(
string email,
string emailVerificationToken,
string masterPasswordHash,
string masterKeyWrappedUserKey,
int iterations,
string publicKey,
string encryptedPrivateKey)
{
// Arrange: Provide only unlock-data KDF + key; leave root KDF fields null
var unlockKdf = new KdfRequestModel
{
KdfType = KdfType.PBKDF2_SHA256,
Iterations = iterations
};
var model = new RegisterFinishRequestModel
{
Email = email,
EmailVerificationToken = emailVerificationToken,
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
{
// present but not used by ToUser for KDF/Key
Kdf = unlockKdf,
MasterPasswordAuthenticationHash = masterPasswordHash,
Salt = email
},
MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel
{
Kdf = unlockKdf,
MasterKeyWrappedUserKey = masterKeyWrappedUserKey,
Salt = email
},
// root KDF fields intentionally null
Kdf = null,
KdfIterations = null,
UserAsymmetricKeys = new KeysRequestModel
{
PublicKey = publicKey,
EncryptedPrivateKey = encryptedPrivateKey
}
};
_registerUserCommand
.RegisterUserViaEmailVerificationToken(Arg.Any<User>(), masterPasswordHash, emailVerificationToken)
.Returns(Task.FromResult(IdentityResult.Success));
// Act
var _ = await _sut.PostRegisterFinish(model);
// Assert: The user passed to command uses unlock-data values
await _registerUserCommand.Received(1).RegisterUserViaEmailVerificationToken(
Arg.Is<User>(u =>
u.Email == email &&
u.Kdf == unlockKdf.KdfType &&
u.KdfIterations == unlockKdf.Iterations &&
u.Key == masterKeyWrappedUserKey),
masterPasswordHash,
emailVerificationToken);
}
[Theory, BitAutoData]
public async Task PostRegisterFinish_LegacyForm_UsesRootFields_WhenUnlockDataNull(
string email,
string emailVerificationToken,
string masterPasswordHash,
string legacyKey,
string publicKey,
string encryptedPrivateKey)
{
// Arrange: Provide only legacy root KDF + key; no unlock-data provided
var model = new RegisterFinishRequestModel
{
Email = email,
EmailVerificationToken = emailVerificationToken,
MasterPasswordHash = masterPasswordHash,
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
UserSymmetricKey = legacyKey,
MasterPasswordUnlock = null,
UserAsymmetricKeys = new KeysRequestModel
{
PublicKey = publicKey,
EncryptedPrivateKey = encryptedPrivateKey
}
};
_registerUserCommand
.RegisterUserViaEmailVerificationToken(Arg.Any<User>(), masterPasswordHash, emailVerificationToken)
.Returns(Task.FromResult(IdentityResult.Success));
// Act
var _ = await _sut.PostRegisterFinish(model);
// Assert: The user passed to command uses root values
await _registerUserCommand.Received(1).RegisterUserViaEmailVerificationToken(
Arg.Is<User>(u =>
u.Email == email &&
u.Kdf == KdfType.PBKDF2_SHA256 &&
u.KdfIterations == AuthConstants.PBKDF2_ITERATIONS.Default &&
u.Key == legacyKey),
masterPasswordHash,
emailVerificationToken);
}
[Theory, BitAutoData]
public void RegisterFinishRequestModel_Validate_Throws_WhenUnlockAndAuthDataMismatch(
string email,
string authHash,
string masterKeyWrappedUserKey,
string publicKey,
string encryptedPrivateKey)
{
// Arrange: authentication and unlock have different KDF and/or salt
var authKdf = new KdfRequestModel
{
KdfType = KdfType.PBKDF2_SHA256,
Iterations = AuthConstants.PBKDF2_ITERATIONS.Default
};
var unlockKdf = new KdfRequestModel
{
KdfType = KdfType.Argon2id,
Iterations = AuthConstants.ARGON2_ITERATIONS.Default,
Memory = AuthConstants.ARGON2_MEMORY.Default,
Parallelism = AuthConstants.ARGON2_PARALLELISM.Default
};
var model = new RegisterFinishRequestModel
{
Email = email,
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
{
Kdf = authKdf,
MasterPasswordAuthenticationHash = authHash,
Salt = email
},
MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel
{
Kdf = unlockKdf,
MasterKeyWrappedUserKey = masterKeyWrappedUserKey,
Salt = email
},
UserAsymmetricKeys = new KeysRequestModel
{
PublicKey = publicKey,
EncryptedPrivateKey = encryptedPrivateKey
}
};
// Provide a minimal valid token type to satisfy model-level token validation
model.EmailVerificationToken = "test-token";
var ctx = new ValidationContext(model);
// Act
var results = model.Validate(ctx).ToList();
// Assert mismatched auth/unlock is allowed
Assert.Empty(results);
}
[Theory, BitAutoData]
public void RegisterFinishRequestModel_Validate_Throws_WhenSaltMismatch(
string email,
string authHash,
string masterKeyWrappedUserKey,
string publicKey,
string encryptedPrivateKey)
{
var unlockKdf = new KdfRequestModel
{
KdfType = KdfType.Argon2id,
Iterations = AuthConstants.ARGON2_ITERATIONS.Default,
Memory = AuthConstants.ARGON2_MEMORY.Default,
Parallelism = AuthConstants.ARGON2_PARALLELISM.Default
};
var model = new RegisterFinishRequestModel
{
Email = email,
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
{
Kdf = unlockKdf,
MasterPasswordAuthenticationHash = authHash,
Salt = email
},
MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel
{
Kdf = unlockKdf,
MasterKeyWrappedUserKey = masterKeyWrappedUserKey,
// Intentionally different salt to force mismatch
Salt = email + ".mismatch"
},
UserAsymmetricKeys = new KeysRequestModel
{
PublicKey = publicKey,
EncryptedPrivateKey = encryptedPrivateKey
}
};
// Provide a minimal valid token type to satisfy model-level token validation
model.EmailVerificationToken = "test-token";
var ctx = new ValidationContext(model);
// Act
var results = model.Validate(ctx).ToList();
// Assert mismatched salts between auth/unlock are allowed
Assert.Empty(results);
}
[Theory, BitAutoData]
public void RegisterFinishRequestModel_Validate_Throws_WhenAuthHashAndRootHashMismatch(
string email,
string authHash,
string differentRootHash,
string masterKeyWrappedUserKey,
string publicKey,
string encryptedPrivateKey)
{
// Arrange: same KDF/salt, but authentication hash differs from legacy root hash
var kdf = new KdfRequestModel
{
KdfType = KdfType.PBKDF2_SHA256,
Iterations = AuthConstants.PBKDF2_ITERATIONS.Default
};
var model = new RegisterFinishRequestModel
{
Email = email,
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
{
Kdf = kdf,
MasterPasswordAuthenticationHash = authHash,
Salt = email
},
MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel
{
Kdf = kdf,
MasterKeyWrappedUserKey = masterKeyWrappedUserKey,
Salt = email
},
// Intentionally set the legacy field to a different value to trigger the throw
MasterPasswordHash = differentRootHash,
UserAsymmetricKeys = new KeysRequestModel
{
PublicKey = publicKey,
EncryptedPrivateKey = encryptedPrivateKey
}
};
// Provide a minimal valid token type to satisfy model-level token validation
model.EmailVerificationToken = "test-token";
var ctx = new ValidationContext(model);
// Act
var results = model.Validate(ctx).ToList();
// Assert: validation result exists with expected message and member names
var mismatchResult = Assert.Single(results.Where(r =>
r.ErrorMessage ==
"MasterPasswordAuthenticationHash and root level MasterPasswordHash provided and are not equal. Only provide one."));
Assert.Contains("MasterPasswordAuthenticationHash", mismatchResult.MemberNames);
Assert.Contains("MasterPasswordHash", mismatchResult.MemberNames);
}
private void SetDefaultKdfHmacKey(byte[]? newKey)
{
var fieldInfo = typeof(AccountsController).GetField("_defaultKdfHmacKey", BindingFlags.NonPublic | BindingFlags.Instance);

View File

@@ -3,13 +3,10 @@
using System.Collections.Concurrent;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using Bit.Core;
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Models.Api.Request;
using Bit.Core.Services;
using Bit.Identity;
using Bit.Test.Common.Helpers;
@@ -26,7 +23,6 @@ public class IdentityApplicationFactory : WebApplicationFactoryBase<Startup>
public const string DefaultDeviceIdentifier = "92b9d953-b9b6-4eaf-9d3e-11d57144dfeb";
public const string DefaultUserEmail = "DefaultEmail@bitwarden.com";
public const string DefaultUserPasswordHash = "default_password_hash";
private const string DefaultEncryptedString = "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98sp4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=";
/// <summary>
/// A dictionary to store registration tokens for email verification. We cannot substitute the IMailService more than once, so
@@ -199,68 +195,6 @@ public class IdentityApplicationFactory : WebApplicationFactoryBase<Startup>
RegisterFinishRequestModel requestModel,
bool marketingEmails = true)
{
// Ensure required fields for registration finish are present.
// Prefer legacy-path defaults (root fields) to minimize changes to tests.
// PM-28143 - When MasterPasswordAuthenticationData is required, delete all handling of MasterPasswordHash.
requestModel.MasterPasswordHash ??= DefaultUserPasswordHash;
// PM-28143 - When KDF is sourced exclusively from MasterPasswordUnlockData, delete the root Kdf defaults below.
requestModel.Kdf ??= KdfType.PBKDF2_SHA256;
requestModel.KdfIterations ??= AuthConstants.PBKDF2_ITERATIONS.Default;
// Ensure a symmetric key is provided when no unlock data is present
// PM-28143 - When MasterPasswordUnlockData is required, delete the UserSymmetricKey fallback block below.
if (requestModel.MasterPasswordUnlock == null && string.IsNullOrWhiteSpace(requestModel.UserSymmetricKey))
{
requestModel.UserSymmetricKey = "user_symmetric_key";
}
// Align unlock/auth data KDF with root KDF so login uses the provided master password hash.
// PM-28143 - After removing root Kdf fields, build KDF exclusively from MasterPasswordUnlockData.Kdf and delete this alignment section.
var effectiveKdfType = requestModel.Kdf ?? KdfType.PBKDF2_SHA256;
var effectiveIterations = requestModel.KdfIterations ?? AuthConstants.PBKDF2_ITERATIONS.Default;
int? effectiveMemory = null;
int? effectiveParallelism = null;
if (effectiveKdfType == KdfType.Argon2id)
{
effectiveIterations = AuthConstants.ARGON2_ITERATIONS.InsideRange(effectiveIterations)
? effectiveIterations
: AuthConstants.ARGON2_ITERATIONS.Default;
effectiveMemory = AuthConstants.ARGON2_MEMORY.Default;
effectiveParallelism = AuthConstants.ARGON2_PARALLELISM.Default;
}
var alignedKdf = new KdfRequestModel
{
KdfType = effectiveKdfType,
Iterations = effectiveIterations,
Memory = effectiveMemory,
Parallelism = effectiveParallelism
};
if (requestModel.MasterPasswordUnlock != null)
{
var unlock = requestModel.MasterPasswordUnlock;
// Always force a valid encrypted string for tests to avoid model validation failures.
requestModel.MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel
{
Kdf = alignedKdf,
MasterKeyWrappedUserKey = unlock.MasterKeyWrappedUserKey,
Salt = string.IsNullOrWhiteSpace(unlock.Salt) ? requestModel.Email : unlock.Salt
};
}
if (requestModel.MasterPasswordAuthentication != null)
{
// Ensure registration uses the same hash the tests will provide at login.
// PM-28143 - When MasterPasswordAuthenticationData is the only source of the auth hash,
// stop overriding it from MasterPasswordHash and delete this whole reassignment block.
requestModel.MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
{
Kdf = alignedKdf,
MasterPasswordAuthenticationHash = requestModel.MasterPasswordHash,
Salt = requestModel.Email
};
}
var sendVerificationEmailReqModel = new RegisterSendVerificationEmailRequestModel
{
Email = requestModel.Email,
@@ -277,11 +211,8 @@ public class IdentityApplicationFactory : WebApplicationFactoryBase<Startup>
requestModel.EmailVerificationToken = RegistrationTokens[requestModel.Email];
var postRegisterFinishHttpContext = await PostRegisterFinishAsync(requestModel);
if (postRegisterFinishHttpContext.Response.StatusCode != StatusCodes.Status200OK)
{
var body = await ReadResponseBodyAsync(postRegisterFinishHttpContext);
Assert.Fail($"register/finish failed (status {postRegisterFinishHttpContext.Response.StatusCode}). Body: {body}");
}
Assert.Equal(StatusCodes.Status200OK, postRegisterFinishHttpContext.Response.StatusCode);
var database = GetDatabaseContext();
var user = await database.Users
@@ -291,32 +222,4 @@ public class IdentityApplicationFactory : WebApplicationFactoryBase<Startup>
return user;
}
private static async Task<string> ReadResponseBodyAsync(HttpContext ctx)
{
try
{
if (ctx?.Response?.Body == null)
{
return "<no body>";
}
var stream = ctx.Response.Body;
if (stream.CanSeek)
{
stream.Seek(0, SeekOrigin.Begin);
}
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true);
var text = await reader.ReadToEndAsync();
if (stream.CanSeek)
{
stream.Seek(0, SeekOrigin.Begin);
}
return string.IsNullOrWhiteSpace(text) ? "<empty body>" : text;
}
catch (Exception ex)
{
return $"<error reading body: {ex.Message}>";
}
}
}