1
0
mirror of https://github.com/bitwarden/server synced 2026-02-21 20:03:40 +00:00

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

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

View File

@@ -1,4 +1,5 @@
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;

View File

@@ -1,11 +1,10 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Utilities;
namespace Bit.Core.KeyManagement.Models.Api.Request;
namespace Bit.Api.KeyManagement.Models.Requests;
public class KdfRequestModel : IValidatableObject
public class KdfRequestModel
{
[Required]
public required KdfType KdfType { get; init; }
@@ -24,10 +23,4 @@ public class KdfRequestModel : IValidatableObject
Parallelism = Parallelism
};
}
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// Generic per-request KDF validation for any request model embedding KdfRequestModel
return KdfSettingsValidator.Validate(ToData());
}
}

View File

@@ -1,12 +1,8 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.KeyManagement.Models.Data;
namespace Bit.Core.KeyManagement.Models.Api.Request;
namespace Bit.Api.KeyManagement.Models.Requests;
/// <summary>
/// Use this datatype when interfacing with requests to create a separation of concern.
/// See <see cref="MasterPasswordAuthenticationData"/> to use for commands, queries, services.
/// </summary>
public class MasterPasswordAuthenticationDataRequestModel
{
public required KdfRequestModel Kdf { get; init; }

View File

@@ -2,12 +2,8 @@
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Utilities;
namespace Bit.Core.KeyManagement.Models.Api.Request;
namespace Bit.Api.KeyManagement.Models.Requests;
/// <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; }

View File

@@ -1,6 +1,6 @@
using Bit.Core.Entities;
#nullable enable
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;
@@ -21,32 +21,19 @@ 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)]
// 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; }
public required string MasterPasswordHash { get; set; }
[StringLength(50)]
public string? MasterPasswordHint { 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 string UserSymmetricKey { get; set; }
public required KeysRequestModel UserAsymmetricKeys { 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 required KdfType Kdf { get; set; }
public required int KdfIterations { get; set; }
public int? KdfMemory { get; set; }
// PM-28143 - Remove line below
public int? KdfParallelism { get; set; }
public Guid? OrganizationUserId { get; set; }
@@ -67,14 +54,11 @@ public class RegisterFinishRequestModel : IValidatableObject
{
Email = Email,
MasterPasswordHint = MasterPasswordHint,
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
Kdf = Kdf,
KdfIterations = KdfIterations,
KdfMemory = KdfMemory,
KdfParallelism = KdfParallelism,
Key = UserSymmetricKey,
};
UserAsymmetricKeys.ToUser(user);
@@ -88,9 +72,7 @@ public class RegisterFinishRequestModel : IValidatableObject
{
return RegisterFinishTokenType.EmailVerification;
}
if (!string.IsNullOrEmpty(OrgInviteToken)
&& OrganizationUserId.HasValue
&& OrganizationUserId.Value != Guid.Empty)
if (!string.IsNullOrEmpty(OrgInviteToken) && OrganizationUserId.HasValue)
{
return RegisterFinishTokenType.OrganizationInvite;
}
@@ -98,15 +80,11 @@ public class RegisterFinishRequestModel : IValidatableObject
{
return RegisterFinishTokenType.OrgSponsoredFreeFamilyPlan;
}
if (!string.IsNullOrWhiteSpace(AcceptEmergencyAccessInviteToken)
&& AcceptEmergencyAccessId.HasValue
&& AcceptEmergencyAccessId.Value != Guid.Empty)
if (!string.IsNullOrWhiteSpace(AcceptEmergencyAccessInviteToken) && AcceptEmergencyAccessId.HasValue)
{
return RegisterFinishTokenType.EmergencyAccessInvite;
}
if (!string.IsNullOrWhiteSpace(ProviderInviteToken)
&& ProviderUserId.HasValue
&& ProviderUserId.Value != Guid.Empty)
if (!string.IsNullOrWhiteSpace(ProviderInviteToken) && ProviderUserId.HasValue)
{
return RegisterFinishTokenType.ProviderInvite;
}
@@ -114,156 +92,9 @@ public class RegisterFinishRequestModel : IValidatableObject
throw new InvalidOperationException("Invalid token type.");
}
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// 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;
}
return KdfSettingsValidator.Validate(Kdf, KdfIterations, KdfMemory, KdfParallelism);
}
}

View File

@@ -7,6 +7,8 @@ using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Identity;
#nullable enable
namespace Bit.Core.Entities;
public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFactorProvidersUser
@@ -49,7 +51,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.
@@ -105,8 +107,6 @@ 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()
{

View File

@@ -1,13 +1,8 @@
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; }

View File

@@ -1,4 +1,5 @@
using Bit.Core.Entities;
#nullable enable
using Bit.Core.Entities;
using Bit.Core.Enums;
namespace Bit.Core.KeyManagement.Models.Data;

View File

@@ -1,13 +1,8 @@
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; }

View File

@@ -6,7 +6,6 @@ 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)

View File

@@ -1,4 +1,8 @@
using System.Text;
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using System.Diagnostics;
using System.Text;
using Bit.Core;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Api.Request.Accounts;
@@ -38,7 +42,7 @@ public class AccountsController : Controller
private readonly IFeatureService _featureService;
private readonly IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> _registrationEmailVerificationTokenDataFactory;
private readonly byte[]? _defaultKdfHmacKey = null;
private readonly byte[] _defaultKdfHmacKey = null;
private static readonly List<UserKdfInformation> _defaultKdfResults =
[
// The first result (index 0) should always return the "normal" default.
@@ -141,55 +145,40 @@ public class AccountsController : Controller
[HttpPost("register/finish")]
public async Task<RegisterFinishResponseModel> PostRegisterFinish([FromBody] RegisterFinishRequestModel model)
{
User user = model.ToUser();
var user = model.ToUser();
// Users will either have an emailed token or an email verification token - not both.
IdentityResult? identityResult = null;
// PM-28143 - Just use the MasterPasswordAuthenticationData.MasterPasswordAuthenticationHash
string masterPasswordAuthenticationHash = model.MasterPasswordAuthentication?.MasterPasswordAuthenticationHash
?? model.MasterPasswordHash!;
IdentityResult identityResult = null;
switch (model.GetTokenType())
{
case RegisterFinishTokenType.EmailVerification:
identityResult = await _registerUserCommand.RegisterUserViaEmailVerificationToken(
user,
masterPasswordAuthenticationHash,
model.EmailVerificationToken!);
return ProcessRegistrationResult(identityResult, user);
identityResult =
await _registerUserCommand.RegisterUserViaEmailVerificationToken(user, model.MasterPasswordHash,
model.EmailVerificationToken);
return ProcessRegistrationResult(identityResult, user);
case RegisterFinishTokenType.OrganizationInvite:
identityResult = await _registerUserCommand.RegisterUserViaOrganizationInviteToken(
user,
masterPasswordAuthenticationHash,
model.OrgInviteToken!,
model.OrganizationUserId);
return ProcessRegistrationResult(identityResult, user);
identityResult = await _registerUserCommand.RegisterUserViaOrganizationInviteToken(user, model.MasterPasswordHash,
model.OrgInviteToken, model.OrganizationUserId);
return ProcessRegistrationResult(identityResult, user);
case RegisterFinishTokenType.OrgSponsoredFreeFamilyPlan:
identityResult = await _registerUserCommand.RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(
user,
masterPasswordAuthenticationHash,
model.OrgSponsoredFreeFamilyPlanToken!);
return ProcessRegistrationResult(identityResult, user);
identityResult = await _registerUserCommand.RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(user, model.MasterPasswordHash, model.OrgSponsoredFreeFamilyPlanToken);
return ProcessRegistrationResult(identityResult, user);
case RegisterFinishTokenType.EmergencyAccessInvite:
identityResult = await _registerUserCommand.RegisterUserViaAcceptEmergencyAccessInviteToken(
user,
masterPasswordAuthenticationHash,
model.AcceptEmergencyAccessInviteToken!,
(Guid)model.AcceptEmergencyAccessId!);
return ProcessRegistrationResult(identityResult, user);
Debug.Assert(model.AcceptEmergencyAccessId.HasValue);
identityResult = await _registerUserCommand.RegisterUserViaAcceptEmergencyAccessInviteToken(user, model.MasterPasswordHash,
model.AcceptEmergencyAccessInviteToken, model.AcceptEmergencyAccessId.Value);
return ProcessRegistrationResult(identityResult, user);
case RegisterFinishTokenType.ProviderInvite:
identityResult = await _registerUserCommand.RegisterUserViaProviderInviteToken(
user,
masterPasswordAuthenticationHash,
model.ProviderInviteToken!,
(Guid)model.ProviderUserId!);
return ProcessRegistrationResult(identityResult, user);
Debug.Assert(model.ProviderUserId.HasValue);
identityResult = await _registerUserCommand.RegisterUserViaProviderInviteToken(user, model.MasterPasswordHash,
model.ProviderInviteToken, model.ProviderUserId.Value);
return ProcessRegistrationResult(identityResult, user);
default:
throw new BadRequestException("Invalid registration finish request");
}