1
0
mirror of https://github.com/bitwarden/server synced 2026-02-07 20:23:49 +00:00

test(register): [PM-27084] Account Register Uses New Data Types - Added validation tests and ToUser no longer throws bad request.

This commit is contained in:
Patrick Pimentel
2026-01-09 09:27:12 -05:00
parent b5dadcd1d3
commit 9e43ca2442
3 changed files with 180 additions and 119 deletions

View File

@@ -1,6 +1,5 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Models.Api.Request;
using Bit.Core.Utilities;
@@ -68,16 +67,14 @@ public class RegisterFinishRequestModel : IValidatableObject
{
Email = Email,
MasterPasswordHint = MasterPasswordHint,
Kdf = MasterPasswordUnlock?.Kdf.KdfType ?? Kdf
?? throw new BadRequestException("KdfType couldn't be found on either the MasterPasswordUnlock or the Kdf property passed in."),
KdfIterations = MasterPasswordUnlock?.Kdf.Iterations ?? KdfIterations
?? throw new BadRequestException("KdfIterations couldn't be found on either the MasterPasswordUnlock or the KdfIterations property passed in."),
Kdf = (KdfType)(MasterPasswordUnlock?.Kdf.KdfType ?? Kdf)!,
KdfIterations = (int)(MasterPasswordUnlock?.Kdf.Iterations ?? KdfIterations)!,
// KdfMemory and KdfParallelism are optional (only used for Argon2id)
KdfMemory = MasterPasswordUnlock?.Kdf.Memory ?? KdfMemory,
KdfParallelism = MasterPasswordUnlock?.Kdf.Parallelism ?? KdfParallelism,
// PM-28827 To be added when MasterPasswordSalt is added to the user column
// MasterPasswordSalt = MasterPasswordUnlock?.Salt ?? Email.ToLower().Trim(),
Key = MasterPasswordUnlock?.MasterKeyWrappedUserKey ?? UserSymmetricKey ?? throw new BadRequestException("MasterKeyWrappedUserKey couldn't be found on either the MasterPasswordUnlockData or the UserSymmetricKey property passed in."),
Key = MasterPasswordUnlock?.MasterKeyWrappedUserKey ?? UserSymmetricKey
};
UserAsymmetricKeys.ToUser(user);

View File

@@ -1,5 +1,6 @@
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;
@@ -7,6 +8,17 @@ 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,
@@ -170,4 +182,169 @@ 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_WhenNeitherAuthNorUnlock_AndValidRootKdf_IsValid()
{
var model = new RegisterFinishRequestModel
{
Email = "user@example.com",
UserAsymmetricKeys = new KeysRequestModel { PublicKey = "pk", EncryptedPrivateKey = "sk" },
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
// Memory and Parallelism irrelevant for PBKDF2
EmailVerificationToken = "token"
};
var results = Validate(model);
Assert.DoesNotContain(results, r => r.ErrorMessage?.Contains("Kdf") == true);
Assert.Empty(results.Where(r => r.ErrorMessage == "No valid registration token provided"));
}
[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

@@ -927,119 +927,6 @@ public class AccountsControllerTests : IDisposable
emailVerificationToken);
}
[Theory, BitAutoData]
public async Task PostRegisterFinish_WhenKdfMissingInAllSources_ShouldReturnBadRequest(
string email,
string emailVerificationToken,
string masterPasswordHash,
string masterKeyWrappedUserKey,
int iterations,
string publicKey,
string encryptedPrivateKey)
{
// Arrange: No KDF at root, and no unlock-data present
var model = new RegisterFinishRequestModel
{
Email = email,
EmailVerificationToken = emailVerificationToken,
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
{
// present but ToUser does not source KDF from here
Kdf = new KdfRequestModel { KdfType = KdfType.Argon2id, Iterations = iterations },
MasterPasswordAuthenticationHash = masterPasswordHash,
Salt = email
},
MasterPasswordUnlock = null,
Kdf = null,
KdfIterations = iterations,
UserSymmetricKey = masterKeyWrappedUserKey,
UserAsymmetricKeys = new KeysRequestModel
{
PublicKey = publicKey,
EncryptedPrivateKey = encryptedPrivateKey
}
};
// Act & Assert
var ex = await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostRegisterFinish(model));
Assert.Equal("KdfType couldn't be found on either the MasterPasswordUnlock or the Kdf property passed in.", ex.Message);
}
[Theory, BitAutoData]
public async Task PostRegisterFinish_WhenKdfIterationsMissingInAllSources_ShouldReturnBadRequest(
string email,
string emailVerificationToken,
string masterPasswordHash,
string masterKeyWrappedUserKey,
KdfType kdfType,
string publicKey,
string encryptedPrivateKey)
{
// Arrange: No KdfIterations at root, and no unlock-data present
var model = new RegisterFinishRequestModel
{
Email = email,
EmailVerificationToken = emailVerificationToken,
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
{
// present but ToUser does not source iterations from here
Kdf = new KdfRequestModel { KdfType = kdfType, Iterations = AuthConstants.PBKDF2_ITERATIONS.Default },
MasterPasswordAuthenticationHash = masterPasswordHash,
Salt = email
},
MasterPasswordUnlock = null,
Kdf = kdfType,
KdfIterations = null,
UserSymmetricKey = masterKeyWrappedUserKey,
UserAsymmetricKeys = new KeysRequestModel
{
PublicKey = publicKey,
EncryptedPrivateKey = encryptedPrivateKey
}
};
// Act & Assert
var ex = await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostRegisterFinish(model));
Assert.Equal("KdfIterations couldn't be found on either the MasterPasswordUnlock or the KdfIterations property passed in.", ex.Message);
}
[Theory, BitAutoData]
public async Task PostRegisterFinish_WhenKeyMissingInAllSources_ShouldReturnBadRequest(
string email,
string emailVerificationToken,
string masterPasswordHash,
int iterations,
KdfType kdfType,
string publicKey,
string encryptedPrivateKey)
{
// Arrange: No key at root, and no unlock-data present
var model = new RegisterFinishRequestModel
{
Email = email,
EmailVerificationToken = emailVerificationToken,
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
{
Kdf = new KdfRequestModel { KdfType = kdfType, Iterations = iterations },
MasterPasswordAuthenticationHash = masterPasswordHash,
Salt = email
},
MasterPasswordUnlock = null,
Kdf = kdfType,
KdfIterations = iterations,
UserSymmetricKey = null,
UserAsymmetricKeys = new KeysRequestModel
{
PublicKey = publicKey,
EncryptedPrivateKey = encryptedPrivateKey
}
};
// Act & Assert
var ex = await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostRegisterFinish(model));
Assert.Equal("MasterKeyWrappedUserKey couldn't be found on either the MasterPasswordUnlockData or the UserSymmetricKey property passed in.", ex.Message);
}
[Theory, BitAutoData]
public void RegisterFinishRequestModel_Validate_Throws_WhenUnlockAndAuthDataMismatch(
string email,