Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
6c7daa6
feat(register): [PM-27084] Account Register Uses New Data Types - Iniโ€ฆ
Patrick-Pimentel-Bitwarden Dec 9, 2025
1535fa3
feat(register): [PM-27084] Account Register Uses New Data Types - Fixโ€ฆ
Patrick-Pimentel-Bitwarden Dec 9, 2025
9000474
fix(register): [PM-27084] Account Register Uses New Data Types - Addeโ€ฆ
Patrick-Pimentel-Bitwarden Dec 10, 2025
d7ddf2e
fix(register): [PM-27084] Account Register Uses New Data Types - Accoโ€ฆ
Patrick-Pimentel-Bitwarden Dec 11, 2025
08f3acd
docs(register): [PM-27084] Account Register Uses New Data Types - Madโ€ฆ
Patrick-Pimentel-Bitwarden Dec 11, 2025
f47dac9
test(register): [PM-27084] Account Register Uses New Data Types - Addโ€ฆ
Patrick-Pimentel-Bitwarden Dec 11, 2025
bb2ba14
Update src/Core/Auth/Models/Api/Request/Accounts/RegisterFinishRequesโ€ฆ
Patrick-Pimentel-Bitwarden Dec 11, 2025
7cb55eb
Merge branch 'main' into auth/pm-27084/register-accepts-new-data-types
Patrick-Pimentel-Bitwarden Dec 11, 2025
b33e1ac
docs(register): [PM-27084] Account Register Uses New Data Types - Addโ€ฆ
Patrick-Pimentel-Bitwarden Dec 11, 2025
c21d7b4
test(register): [PM-27084] Account Register Uses New Data Types - Tesโ€ฆ
Patrick-Pimentel-Bitwarden Dec 16, 2025
aa8e8cc
test(register): [PM-27084] Account Register Uses New Data Types - Fixโ€ฆ
Patrick-Pimentel-Bitwarden Dec 16, 2025
5b2edef
test(register): [PM-27084] Account Register Uses New Data Types - Addโ€ฆ
Patrick-Pimentel-Bitwarden Dec 23, 2025
57f69ad
Merge branch 'main' into auth/pm-27084/register-accepts-new-data-types
Patrick-Pimentel-Bitwarden Dec 23, 2025
4f81d75
Update src/Core/Auth/Models/Api/Request/Accounts/RegisterFinishRequesโ€ฆ
Patrick-Pimentel-Bitwarden Dec 23, 2025
fac1d4b
fix(register): [PM-27084] Account Register Uses New Data Types - Addeโ€ฆ
Patrick-Pimentel-Bitwarden Dec 29, 2025
2d9786e
fix(register): [PM-27084] Account Register Uses New Data Types - Fixeโ€ฆ
Patrick-Pimentel-Bitwarden Dec 29, 2025
cc895d0
fix(register): [PM-27084] Account Register Uses New Data Types - Addeโ€ฆ
Patrick-Pimentel-Bitwarden Dec 29, 2025
4475649
fix(register): [PM-27084] Account Register Uses New Data Types - Remoโ€ฆ
Patrick-Pimentel-Bitwarden Dec 30, 2025
a705ea4
fix(register): [PM-27084] Account Register Uses New Data Types - Shufโ€ฆ
Patrick-Pimentel-Bitwarden Dec 31, 2025
227f3a0
comment(register): [PM-27084] Account Register Uses New Data Types - โ€ฆ
Patrick-Pimentel-Bitwarden Dec 31, 2025
fb54d21
comment(register): [PM-27084] Account Register Uses New Data Types - โ€ฆ
Patrick-Pimentel-Bitwarden Dec 31, 2025
b0cce70
comment(register): [PM-27084] Account Register Uses New Data Types - โ€ฆ
Patrick-Pimentel-Bitwarden Dec 31, 2025
af2f704
comment(register): [PM-27084] Account Register Uses New Data Types - โ€ฆ
Patrick-Pimentel-Bitwarden Dec 31, 2025
015d2ed
comment(register): [PM-27084] Account Register Uses New Data Types - โ€ฆ
Patrick-Pimentel-Bitwarden Dec 31, 2025
67ff0da
Merge branch 'main' into auth/pm-27084/register-accepts-new-data-types
Patrick-Pimentel-Bitwarden Dec 31, 2025
fc507a4
comment(register): [PM-27084] Account Register Uses New Data Types - โ€ฆ
Patrick-Pimentel-Bitwarden Dec 31, 2025
05d8cc5
test(register): [PM-27084] Account Register Uses New Data Types - Fixโ€ฆ
Patrick-Pimentel-Bitwarden Dec 31, 2025
e531ab1
test(register): [PM-27084] Account Register Uses New Data Types - Fixโ€ฆ
Patrick-Pimentel-Bitwarden Dec 31, 2025
28640d0
fix(register): [PM-27084] Account Register Uses New Data Types - Addrโ€ฆ
Patrick-Pimentel-Bitwarden Jan 1, 2026
260b289
fix(register): [PM-27084] Account Register Uses New Data Types - Fixeโ€ฆ
Patrick-Pimentel-Bitwarden Jan 2, 2026
06bf7b8
fix(register): [PM-27084] Account Register Uses New Data Types - Convโ€ฆ
Patrick-Pimentel-Bitwarden Jan 2, 2026
c255e39
fix(register): [PM-27084] Account Register Uses New Data Types - Addeโ€ฆ
Patrick-Pimentel-Bitwarden Jan 2, 2026
3aa0e4c
fix(register): [PM-27084] Account Register Uses New Data Types - Shufโ€ฆ
Patrick-Pimentel-Bitwarden Jan 2, 2026
2111df7
fix(register): [PM-27084] Account Register Uses New Data Types - Remoโ€ฆ
Patrick-Pimentel-Bitwarden Jan 2, 2026
65c0ac9
fix(register): [PM-27084] Account Register Uses New Data Types - Remoโ€ฆ
Patrick-Pimentel-Bitwarden Jan 2, 2026
a5890d2
Merge branch 'main' into auth/pm-27084/register-accepts-new-data-types
Patrick-Pimentel-Bitwarden Jan 2, 2026
b5dadcd
Merge branch 'main' into auth/pm-27084/register-accepts-new-data-types
Patrick-Pimentel-Bitwarden Jan 5, 2026
9e43ca2
test(register): [PM-27084] Account Register Uses New Data Types - Addโ€ฆ
Patrick-Pimentel-Bitwarden Jan 9, 2026
93c9631
test(register): [PM-27084] Account Register Uses New Data Types - Addโ€ฆ
Patrick-Pimentel-Bitwarden Jan 12, 2026
f73904d
Merge remote-tracking branch 'origin' into auth/pm-27084/register-accโ€ฆ
Patrick-Pimentel-Bitwarden Jan 12, 2026
f92d7d2
test(register): [PM-27084] Account Register Uses New Data Types - Addโ€ฆ
Patrick-Pimentel-Bitwarden Jan 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
๏ปฟusing System.ComponentModel.DataAnnotations;
using Bit.Api.KeyManagement.Models.Requests;
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Entities;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
๏ปฟ#nullable enable
using Bit.Core.Entities;
๏ปฟusing Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Models.Api.Request;
using Bit.Core.Utilities;

namespace Bit.Core.Auth.Models.Api.Request.Accounts;
Expand All @@ -21,19 +21,32 @@ public class RegisterFinishRequestModel : IValidatableObject
public required string Email { get; set; }
public string? EmailVerificationToken { 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)]
public required string MasterPasswordHash { get; set; }
// Made optional but there will still be a thrown error if it does not exist either here or
// in the MasterPasswordAuthenticationData.
public string? MasterPasswordHash { get; set; }

[StringLength(50)]
public string? MasterPasswordHint { get; set; }

public required string UserSymmetricKey { get; set; }
// PM-28143 - Remove property below (made optional during migration to MasterPasswordUnlockData)
// Made optional but there will still be a thrown error if it does not exist either here or
// in the MasterPasswordAuthenticationData.
public string? UserSymmetricKey { get; set; }

public required KeysRequestModel UserAsymmetricKeys { get; set; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โ„น๏ธ This is outside of the scope of this PR, but fyi, for account registration V2, instead of UserAsymmetricKeys (will become optional for V1 compatibility), we will use AccountKeysRequestModel

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for sharing that knowledge, helps me understand future changes.


public required KdfType Kdf { get; set; }
public required int KdfIterations { get; set; }
// PM-28143 - Remove line below (made optional during migration to MasterPasswordUnlockData)
public KdfType? Kdf { get; set; }
// PM-28143 - Remove line below (made optional during migration to MasterPasswordUnlockData)
public int? KdfIterations { get; set; }
// PM-28143 - Remove line below
public int? KdfMemory { get; set; }
// PM-28143 - Remove line below
public int? KdfParallelism { get; set; }

public Guid? OrganizationUserId { get; set; }
Expand All @@ -54,11 +67,14 @@ public User ToUser()
{
Email = Email,
MasterPasswordHint = MasterPasswordHint,
Kdf = Kdf,
KdfIterations = KdfIterations,
KdfMemory = KdfMemory,
KdfParallelism = KdfParallelism,
Key = UserSymmetricKey,
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
};

UserAsymmetricKeys.ToUser(user);
Expand All @@ -72,29 +88,182 @@ public RegisterFinishTokenType GetTokenType()
{
return RegisterFinishTokenType.EmailVerification;
}
if (!string.IsNullOrEmpty(OrgInviteToken) && OrganizationUserId.HasValue)
if (!string.IsNullOrEmpty(OrgInviteToken)
&& OrganizationUserId.HasValue
&& OrganizationUserId.Value != Guid.Empty)
{
return RegisterFinishTokenType.OrganizationInvite;
}
if (!string.IsNullOrWhiteSpace(OrgSponsoredFreeFamilyPlanToken))
{
return RegisterFinishTokenType.OrgSponsoredFreeFamilyPlan;
}
if (!string.IsNullOrWhiteSpace(AcceptEmergencyAccessInviteToken) && AcceptEmergencyAccessId.HasValue)
if (!string.IsNullOrWhiteSpace(AcceptEmergencyAccessInviteToken)
&& AcceptEmergencyAccessId.HasValue
&& AcceptEmergencyAccessId.Value != Guid.Empty)
{
return RegisterFinishTokenType.EmergencyAccessInvite;
}
if (!string.IsNullOrWhiteSpace(ProviderInviteToken) && ProviderUserId.HasValue)
if (!string.IsNullOrWhiteSpace(ProviderInviteToken)
&& ProviderUserId.HasValue
&& ProviderUserId.Value != Guid.Empty)
{
return RegisterFinishTokenType.ProviderInvite;
}

throw new InvalidOperationException("Invalid token type.");
}


public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
return KdfSettingsValidator.Validate(Kdf, KdfIterations, KdfMemory, KdfParallelism);
// 1. Authentication data containing hash and hash at root level check
if (MasterPasswordAuthentication != null && MasterPasswordHash != null)
{
if (MasterPasswordAuthentication.MasterPasswordAuthenticationHash != MasterPasswordHash)
{
yield return new ValidationResult(
$"{nameof(MasterPasswordAuthentication.MasterPasswordAuthenticationHash)} and root level {nameof(MasterPasswordHash)} provided and are not equal. Only provide one.",
[nameof(MasterPasswordAuthentication.MasterPasswordAuthenticationHash), nameof(MasterPasswordHash)]);
}
} // 1.5 if there is no master password hash that is unacceptable even though they are both optional in the model
else if (MasterPasswordAuthentication == null && MasterPasswordHash == null)
{
yield return new ValidationResult(
$"{nameof(MasterPasswordAuthentication.MasterPasswordAuthenticationHash)} and {nameof(MasterPasswordHash)} not found on request, one needs to be defined.",
[nameof(MasterPasswordAuthentication.MasterPasswordAuthenticationHash), nameof(MasterPasswordHash)]);
}

// 2. Validate kdf settings.
if (MasterPasswordUnlock != null)
{
foreach (var validationResult in KdfSettingsValidator.Validate(MasterPasswordUnlock.ToData().Kdf))
{
yield return validationResult;
}
}

if (MasterPasswordAuthentication != null)
{
foreach (var validationResult in KdfSettingsValidator.Validate(MasterPasswordAuthentication.ToData().Kdf))
{
yield return validationResult;
}
}

// 3. Validate root kdf values if kdf values are not in the unlock and authentication.
if (MasterPasswordUnlock == null && MasterPasswordAuthentication == null)
{
var hasMissingRequiredKdfInputs = false;
if (Kdf == null)
{
yield return new ValidationResult($"{nameof(Kdf)} not found on RequestModel", [nameof(Kdf)]);
hasMissingRequiredKdfInputs = true;
}
if (KdfIterations == null)
{
yield return new ValidationResult($"{nameof(KdfIterations)} not found on RequestModel", [nameof(KdfIterations)]);
hasMissingRequiredKdfInputs = true;
}

if (!hasMissingRequiredKdfInputs)
{
foreach (var validationResult in KdfSettingsValidator.Validate(
Kdf!.Value,
KdfIterations!.Value,
KdfMemory,
KdfParallelism))
{
yield return validationResult;
}
}
}
else if (MasterPasswordUnlock == null && MasterPasswordAuthentication != null)
{
// Authentication provided but Unlock missing
yield return new ValidationResult($"{nameof(MasterPasswordUnlock)} not found on RequestModel", [nameof(MasterPasswordUnlock)]);
}
else if (MasterPasswordUnlock != null && MasterPasswordAuthentication == null)
{
// Unlock provided but Authentication missing
yield return new ValidationResult($"{nameof(MasterPasswordAuthentication)} not found on RequestModel", [nameof(MasterPasswordAuthentication)]);
}

// 3. Lastly, validate access token type and presence. Must be done last because of yield break.
RegisterFinishTokenType tokenType;
var tokenTypeResolved = true;
try
{
tokenType = GetTokenType();
}
catch (InvalidOperationException)
{
tokenTypeResolved = false;
tokenType = default;
}

if (!tokenTypeResolved)
{
yield return new ValidationResult("No valid registration token provided");
yield break;
}

switch (tokenType)
{
case RegisterFinishTokenType.EmailVerification:
if (string.IsNullOrEmpty(EmailVerificationToken))
{
yield return new ValidationResult(
$"{nameof(EmailVerificationToken)} absent when processing register/finish.",
[nameof(EmailVerificationToken)]);
}
break;
case RegisterFinishTokenType.OrganizationInvite:
if (string.IsNullOrEmpty(OrgInviteToken))
{
yield return new ValidationResult(
$"{nameof(OrgInviteToken)} absent when processing register/finish.",
[nameof(OrgInviteToken)]);
}
break;
case RegisterFinishTokenType.OrgSponsoredFreeFamilyPlan:
if (string.IsNullOrEmpty(OrgSponsoredFreeFamilyPlanToken))
{
yield return new ValidationResult(
$"{nameof(OrgSponsoredFreeFamilyPlanToken)} absent when processing register/finish.",
[nameof(OrgSponsoredFreeFamilyPlanToken)]);
}
break;
case RegisterFinishTokenType.EmergencyAccessInvite:
if (string.IsNullOrEmpty(AcceptEmergencyAccessInviteToken))
{
yield return new ValidationResult(
$"{nameof(AcceptEmergencyAccessInviteToken)} absent when processing register/finish.",
[nameof(AcceptEmergencyAccessInviteToken)]);
}
if (!AcceptEmergencyAccessId.HasValue || AcceptEmergencyAccessId.Value == Guid.Empty)
{
yield return new ValidationResult(
$"{nameof(AcceptEmergencyAccessId)} absent when processing register/finish.",
[nameof(AcceptEmergencyAccessId)]);
}
break;
case RegisterFinishTokenType.ProviderInvite:
if (string.IsNullOrEmpty(ProviderInviteToken))
{
yield return new ValidationResult(
$"{nameof(ProviderInviteToken)} absent when processing register/finish.",
[nameof(ProviderInviteToken)]);
}
if (!ProviderUserId.HasValue || ProviderUserId.Value == Guid.Empty)
{
yield return new ValidationResult(
$"{nameof(ProviderUserId)} absent when processing register/finish.",
[nameof(ProviderUserId)]);
}
break;
default:
yield return new ValidationResult("Invalid registration finish request");
break;
}
}
}
6 changes: 3 additions & 3 deletions src/Core/Entities/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Identity;

#nullable enable

namespace Bit.Core.Entities;

public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFactorProvidersUser
Expand Down Expand Up @@ -51,7 +49,7 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
public string? Key { get; set; }
/// <summary>
/// The raw public key, without a signature from the user's signature key.
/// </summary>
/// </summary>
public string? PublicKey { get; set; }
/// <summary>
/// User key wrapped private key.
Expand Down Expand Up @@ -107,6 +105,8 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
public DateTime? LastKeyRotationDate { get; set; }
public DateTime? LastEmailChangeDate { get; set; }
public bool VerifyDevices { get; set; } = true;
// PM-28827 Uncomment below line.
// public string? MasterPasswordSalt { get; set; }

public string GetMasterPasswordSalt()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
๏ปฟusing System.ComponentModel.DataAnnotations;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Utilities;

namespace Bit.Api.KeyManagement.Models.Requests;
namespace Bit.Core.KeyManagement.Models.Api.Request;

public class KdfRequestModel
public class KdfRequestModel : IValidatableObject
{
[Required]
public required KdfType KdfType { get; init; }
Expand All @@ -23,4 +24,10 @@ public KdfSettings ToData()
Parallelism = Parallelism
};
}

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// Generic per-request KDF validation for any request model embedding KdfRequestModel
return KdfSettingsValidator.Validate(ToData());
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
๏ปฟusing System.ComponentModel.DataAnnotations;
using Bit.Core.KeyManagement.Models.Data;

namespace Bit.Api.KeyManagement.Models.Requests;
namespace Bit.Core.KeyManagement.Models.Api.Request;

/// <summary>
/// Use this datatype when interfacing with requests to create a separation of concern.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if this comment adds anything. The request and data models is rather known separation of concern, but not documented anywhere in contributing docs. Maybe that's the right place to put it ? Opinions @bitwarden/team-key-management-dev ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, this comment doesn't really add anything.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, worth to note, there is a PR that mentions this in contributing docs, so this comment and all other about the separation of concern is now redundant. https://github.com/bitwarden/contributing-docs/pull/740/changes#diff-360b9394971135ffb743be955a3db753636cafe84b67bcfdd052b77903511556R164

/// See <see cref="MasterPasswordAuthenticationData"/> to use for commands, queries, services.
/// </summary>
public class MasterPasswordAuthenticationDataRequestModel
{
public required KdfRequestModel Kdf { get; init; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Utilities;

namespace Bit.Api.KeyManagement.Models.Requests;
namespace Bit.Core.KeyManagement.Models.Api.Request;

/// <summary>
/// Use this datatype when interfacing with requests to create a separation of concern.
/// See <see cref="MasterPasswordUnlockData"/> to use for commands, queries, services.
/// </summary>
public class MasterPasswordUnlockDataRequestModel
{
public required KdfRequestModel Kdf { get; init; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
๏ปฟusing Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Models.Api.Request;

namespace Bit.Core.KeyManagement.Models.Data;

/// <summary>
/// Use this datatype when interfacing with commands, queries, services to create a separation of concern.
/// See <see cref="MasterPasswordAuthenticationDataRequestModel"/> to use for requests.
/// </summary>
public class MasterPasswordAuthenticationData
{
public required KdfSettings Kdf { get; init; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
๏ปฟ#nullable enable
using Bit.Core.Entities;
๏ปฟusing Bit.Core.Entities;
using Bit.Core.Enums;

namespace Bit.Core.KeyManagement.Models.Data;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
๏ปฟusing Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Models.Api.Request;

namespace Bit.Core.KeyManagement.Models.Data;

/// <summary>
/// Use this datatype when interfacing with commands, queries, services to create a separation of concern.
/// See <see cref="MasterPasswordUnlockDataRequestModel"/> to use for requests.
/// </summary>
public class MasterPasswordUnlockData
{
public required KdfSettings Kdf { get; init; }
Expand Down
1 change: 1 addition & 0 deletions src/Core/Utilities/KdfSettingsValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace Bit.Core.Utilities;

public static class KdfSettingsValidator
{
// PM-28143 - Remove below when fixing ticket
public static IEnumerable<ValidationResult> Validate(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism)
{
switch (kdfType)
Expand Down
Loading
Loading