1
0
mirror of https://github.com/bitwarden/server synced 2026-01-28 15:23:38 +00:00

fix(register): [PM-27084] Account Register Uses New Data Types - Fixed some tests up.

This commit is contained in:
Patrick Pimentel
2025-12-29 13:20:58 -05:00
parent fac1d4bdc2
commit 2d9786e09d
10 changed files with 123 additions and 119 deletions

View File

@@ -1,7 +1,5 @@
#nullable enable
using System.ComponentModel.DataAnnotations;
using Bit.Api.KeyManagement.Models.Requests;
using System.ComponentModel.DataAnnotations;
using Bit.Core.KeyManagement.Models.Api.Request;
namespace Bit.Api.Auth.Models.Request.Accounts;

View File

@@ -1,22 +0,0 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Utilities;
namespace Bit.Api.KeyManagement.Models.Requests;
public class MasterPasswordUnlockDataRequestModel
{
public required KdfRequestModel Kdf { get; init; }
[EncryptedString] public required string MasterKeyWrappedUserKey { get; init; }
[StringLength(256)] public required string Salt { get; init; }
public MasterPasswordUnlockData ToData()
{
return new MasterPasswordUnlockData
{
Kdf = Kdf.ToData(),
MasterKeyWrappedUserKey = MasterKeyWrappedUserKey,
Salt = Salt
};
}
}

View File

@@ -1,6 +1,6 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.Models.Api.Request;
using Bit.Core.Utilities;
namespace Bit.Core.Auth.Models.Api.Request.Accounts;
@@ -21,8 +21,8 @@ public class RegisterFinishRequestModel : IValidatableObject
public required string Email { get; set; }
public string? EmailVerificationToken { get; set; }
public MasterPasswordAuthenticationData? MasterPasswordAuthentication { get; set; }
public MasterPasswordUnlockData? MasterPasswordUnlock { get; set; }
public MasterPasswordAuthenticationDataRequestModel? MasterPasswordAuthentication { get; set; }
public MasterPasswordUnlockDataRequestModel? MasterPasswordUnlock { get; set; }
// PM-28143 - Remove property below (made optional during migration to MasterPasswordUnlockData)
[StringLength(1000)]
@@ -66,10 +66,10 @@ public class RegisterFinishRequestModel : IValidatableObject
// PM-28143 - Remove line below
// When we process this request to a user object, check if the unlock and authentication
// data has been passed through, and if so they should have matching values.
MasterPasswordUnlockData.ThrowIfExistsAndNotMatchingAuthenticationData(MasterPasswordAuthentication, MasterPasswordUnlock);
MasterPasswordUnlockDataRequestModel.ThrowIfExistsAndNotMatchingAuthenticationData(MasterPasswordAuthentication, MasterPasswordUnlock);
// PM-28143 - Remove line below
MasterPasswordAuthenticationData.ThrowIfExistsAndHashIsNotEqual(MasterPasswordAuthentication, MasterPasswordHash);
MasterPasswordAuthenticationDataRequestModel.ThrowIfExistsAndHashIsNotEqual(MasterPasswordAuthentication, MasterPasswordHash);
var user = new User
{
@@ -122,10 +122,10 @@ public class RegisterFinishRequestModel : IValidatableObject
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// PM-28143 - Remove line below
MasterPasswordUnlockData.ThrowIfExistsAndNotMatchingAuthenticationData(MasterPasswordAuthentication, MasterPasswordUnlock);
MasterPasswordUnlockDataRequestModel.ThrowIfExistsAndNotMatchingAuthenticationData(MasterPasswordAuthentication, MasterPasswordUnlock);
// PM-28143 - Remove line below
MasterPasswordAuthenticationData.ThrowIfExistsAndHashIsNotEqual(MasterPasswordAuthentication, MasterPasswordHash);
MasterPasswordAuthenticationDataRequestModel.ThrowIfExistsAndHashIsNotEqual(MasterPasswordAuthentication, MasterPasswordHash);
// PM-28143 - Remove line below
var kdf = MasterPasswordUnlock?.Kdf.KdfType

View File

@@ -1,7 +1,7 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.KeyManagement.Models.Data;
namespace Bit.Api.KeyManagement.Models.Requests;
namespace Bit.Core.KeyManagement.Models.Api.Request;
public class MasterPasswordAuthenticationDataRequestModel
{
@@ -18,4 +18,17 @@ public class MasterPasswordAuthenticationDataRequestModel
Salt = Salt
};
}
public static void ThrowIfExistsAndHashIsNotEqual(
MasterPasswordAuthenticationDataRequestModel? authenticationData,
string? hash)
{
if (authenticationData != null && hash != null)
{
if (authenticationData.MasterPasswordAuthenticationHash != hash)
{
throw new Exception("Master password hash and hash are not equal.");
}
}
}
}

View File

@@ -0,0 +1,49 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Utilities;
namespace Bit.Core.KeyManagement.Models.Api.Request;
public class MasterPasswordUnlockDataRequestModel
{
public required KdfRequestModel Kdf { get; init; }
[EncryptedString] public required string MasterKeyWrappedUserKey { get; init; }
[StringLength(256)] public required string Salt { get; init; }
public MasterPasswordUnlockData ToData()
{
return new MasterPasswordUnlockData
{
Kdf = Kdf.ToData(),
MasterKeyWrappedUserKey = MasterKeyWrappedUserKey,
Salt = Salt
};
}
public static void ThrowIfExistsAndNotMatchingAuthenticationData(
MasterPasswordAuthenticationDataRequestModel? authenticationData,
MasterPasswordUnlockDataRequestModel? unlockData)
{
if (unlockData != null && authenticationData != null)
{
var matches = MatchesAuthenticationData(
unlockData,
authenticationData);
if (!matches)
{
throw new Exception("KDF settings and salt must match between authentication and unlock data.");
}
}
}
private static bool MatchesAuthenticationData(
MasterPasswordUnlockDataRequestModel unlockData,
MasterPasswordAuthenticationDataRequestModel authenticationData)
{
var kdfMatches = unlockData.Kdf.Equals(authenticationData.Kdf);
var saltMatches = unlockData.Salt == authenticationData.Salt;
return kdfMatches && saltMatches;
}
}

View File

@@ -1,8 +1,7 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Models.Data;
namespace Bit.Api.KeyManagement.Models.Requests;
namespace Bit.Core.KeyManagement.Models.Data;
public class KdfRequestModel
{

View File

@@ -3,6 +3,12 @@ using Bit.Core.Exceptions;
namespace Bit.Core.KeyManagement.Models.Data;
/// <summary>
/// The data used for authentication of a master password.
///
/// This data model does not have any validation, consider using MasterPasswordAuthenticationDataRequestModel
/// if validation is required.
/// </summary>
public class MasterPasswordAuthenticationData
{
public required KdfSettings Kdf { get; init; }
@@ -16,17 +22,4 @@ public class MasterPasswordAuthenticationData
throw new BadRequestException("Invalid master password salt.");
}
}
public static void ThrowIfExistsAndHashIsNotEqual(
MasterPasswordAuthenticationData? authenticationData,
string? hash)
{
if (authenticationData != null && hash != null)
{
if (authenticationData.MasterPasswordAuthenticationHash != hash)
{
throw new Exception("Master password hash and hash are not equal.");
}
}
}
}

View File

@@ -16,31 +16,4 @@ public class MasterPasswordUnlockData
throw new BadRequestException("Invalid master password salt.");
}
}
public static void ThrowIfExistsAndNotMatchingAuthenticationData(
MasterPasswordAuthenticationData? authenticationData,
MasterPasswordUnlockData? unlockData)
{
if (unlockData != null && authenticationData != null)
{
var matches = MatchesAuthenticationData(
unlockData,
authenticationData);
if (!matches)
{
throw new Exception("KDF settings and salt must match between authentication and unlock data.");
}
}
}
private static bool MatchesAuthenticationData(
MasterPasswordUnlockData unlockData,
MasterPasswordAuthenticationData authenticationData)
{
var kdfMatches = unlockData.Kdf.Equals(authenticationData.Kdf);
var saltMatches = unlockData.Salt == authenticationData.Salt;
return kdfMatches && saltMatches;
}
}

View File

@@ -9,6 +9,7 @@ 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.KeyManagement.Models.Data;
using Bit.Core.Models.Data;
using Bit.Core.Repositories;
@@ -603,31 +604,28 @@ public class AccountsControllerTests : IDisposable
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 MasterPasswordAuthenticationData
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
{
Kdf = new KdfSettings
{
KdfType = KdfType.Argon2id,
Iterations = AuthConstants.ARGON2_ITERATIONS.Default,
Memory = AuthConstants.ARGON2_MEMORY.Default,
Parallelism = AuthConstants.ARGON2_PARALLELISM.Default
},
Kdf = kdfData,
MasterPasswordAuthenticationHash = masterPasswordHash,
Salt = email // salt choice is not validated here during registration
},
MasterPasswordUnlock = new MasterPasswordUnlockData
MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel
{
Kdf = new KdfSettings
{
KdfType = KdfType.Argon2id,
Iterations = AuthConstants.ARGON2_ITERATIONS.Default,
Memory = AuthConstants.ARGON2_MEMORY.Default,
Parallelism = AuthConstants.ARGON2_PARALLELISM.Default
},
Kdf = kdfData,
MasterKeyWrappedUserKey = masterKeyWrappedUserKey,
Salt = email
},
@@ -719,29 +717,29 @@ public class AccountsControllerTests : IDisposable
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 MasterPasswordAuthenticationData
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
{
Kdf = new KdfSettings
{
KdfType = KdfType.PBKDF2_SHA256,
Iterations = AuthConstants.PBKDF2_ITERATIONS.Default
},
Kdf = kdfData,
MasterPasswordAuthenticationHash = masterPasswordHash,
Salt = email
},
MasterPasswordUnlock = new MasterPasswordUnlockData
MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel
{
Kdf = new KdfSettings
{
KdfType = KdfType.PBKDF2_SHA256,
Iterations = AuthConstants.PBKDF2_ITERATIONS.Default
},
Kdf = kdfData,
MasterKeyWrappedUserKey = masterKeyWrappedUserKey,
Salt = email
},
@@ -759,8 +757,10 @@ public class AccountsControllerTests : IDisposable
OrgInviteToken = orgInviteToken,
OrganizationUserId = organizationUserId,
MasterPasswordHash = masterPasswordHash,
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
Kdf = kdfData.KdfType,
KdfIterations = kdfData.Iterations,
KdfMemory = kdfData.Memory,
KdfParallelism = kdfData.Parallelism,
UserSymmetricKey = masterKeyWrappedUserKey,
UserAsymmetricKeys = new KeysRequestModel
{
@@ -832,7 +832,7 @@ public class AccountsControllerTests : IDisposable
string encryptedPrivateKey)
{
// Arrange: Provide only unlock-data KDF + key; leave root KDF fields null
var unlockKdf = new KdfSettings
var unlockKdf = new KdfRequestModel
{
KdfType = KdfType.PBKDF2_SHA256,
Iterations = iterations
@@ -842,14 +842,14 @@ public class AccountsControllerTests : IDisposable
{
Email = email,
EmailVerificationToken = emailVerificationToken,
MasterPasswordAuthentication = new MasterPasswordAuthenticationData
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
{
// present but not used by ToUser for KDF/Key
Kdf = new KdfSettings { KdfType = KdfType.Argon2id, Iterations = iterations },
Kdf = unlockKdf,
MasterPasswordAuthenticationHash = masterPasswordHash,
Salt = email
},
MasterPasswordUnlock = new MasterPasswordUnlockData
MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel
{
Kdf = unlockKdf,
MasterKeyWrappedUserKey = masterKeyWrappedUserKey,
@@ -942,10 +942,10 @@ public class AccountsControllerTests : IDisposable
{
Email = email,
EmailVerificationToken = emailVerificationToken,
MasterPasswordAuthentication = new MasterPasswordAuthenticationData
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
{
// present but ToUser does not source KDF from here
Kdf = new KdfSettings { KdfType = KdfType.Argon2id, Iterations = iterations },
Kdf = new KdfRequestModel { KdfType = KdfType.Argon2id, Iterations = iterations },
MasterPasswordAuthenticationHash = masterPasswordHash,
Salt = email
},
@@ -980,10 +980,10 @@ public class AccountsControllerTests : IDisposable
{
Email = email,
EmailVerificationToken = emailVerificationToken,
MasterPasswordAuthentication = new MasterPasswordAuthenticationData
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
{
// present but ToUser does not source iterations from here
Kdf = new KdfSettings { KdfType = kdfType, Iterations = AuthConstants.PBKDF2_ITERATIONS.Default },
Kdf = new KdfRequestModel { KdfType = kdfType, Iterations = AuthConstants.PBKDF2_ITERATIONS.Default },
MasterPasswordAuthenticationHash = masterPasswordHash,
Salt = email
},
@@ -1018,9 +1018,9 @@ public class AccountsControllerTests : IDisposable
{
Email = email,
EmailVerificationToken = emailVerificationToken,
MasterPasswordAuthentication = new MasterPasswordAuthenticationData
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
{
Kdf = new KdfSettings { KdfType = kdfType, Iterations = iterations },
Kdf = new KdfRequestModel { KdfType = kdfType, Iterations = iterations },
MasterPasswordAuthenticationHash = masterPasswordHash,
Salt = email
},

View File

@@ -8,6 +8,7 @@ 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.KeyManagement.Models.Data;
using Bit.Core.Services;
using Bit.Identity;
@@ -226,7 +227,7 @@ public class IdentityApplicationFactory : WebApplicationFactoryBase<Startup>
effectiveParallelism = AuthConstants.ARGON2_PARALLELISM.Default;
}
var alignedKdf = new KdfSettings
var alignedKdf = new KdfRequestModel
{
KdfType = effectiveKdfType,
Iterations = effectiveIterations,
@@ -241,7 +242,7 @@ public class IdentityApplicationFactory : WebApplicationFactoryBase<Startup>
var masterKeyWrappedUserKey = !string.IsNullOrWhiteSpace(unlock.MasterKeyWrappedUserKey)
? unlock.MasterKeyWrappedUserKey
: (string.IsNullOrWhiteSpace(requestModel.UserSymmetricKey) ? "user_symmetric_key" : requestModel.UserSymmetricKey);
requestModel.MasterPasswordUnlock = new MasterPasswordUnlockData
requestModel.MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel
{
Kdf = alignedKdf,
MasterKeyWrappedUserKey = masterKeyWrappedUserKey,
@@ -254,7 +255,7 @@ public class IdentityApplicationFactory : WebApplicationFactoryBase<Startup>
// 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 MasterPasswordAuthenticationData
requestModel.MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
{
Kdf = alignedKdf,
MasterPasswordAuthenticationHash = requestModel.MasterPasswordHash,