1
0
mirror of https://github.com/bitwarden/server synced 2025-12-06 00:03:34 +00:00

Revert "feat(prelogin): [Auth/PM-27062] Prelogin New Response (#6577)" (#6582)

This reverts commit 92e511284b.

Merged without feature flag code and before QA could get their review done.
This commit is contained in:
Patrick-Pimentel-Bitwarden
2025-11-14 16:42:14 -05:00
committed by GitHub
parent 92e511284b
commit 7eaca9bb7d
6 changed files with 47 additions and 175 deletions

View File

@@ -4,8 +4,8 @@ namespace Bit.Core.Models.Data;
public class UserKdfInformation public class UserKdfInformation
{ {
public required KdfType Kdf { get; set; } public KdfType Kdf { get; set; }
public required int KdfIterations { get; set; } public int KdfIterations { get; set; }
public int? KdfMemory { get; set; } public int? KdfMemory { get; set; }
public int? KdfParallelism { get; set; } public int? KdfParallelism { get; set; }
} }

View File

@@ -195,35 +195,16 @@ public class AccountsController : Controller
throw new BadRequestException(ModelState); throw new BadRequestException(ModelState);
} }
// Moved from API, If you modify this endpoint, please update API as well. Self hosted installs still use the API endpoints.
[HttpPost("prelogin")] [HttpPost("prelogin")]
[Obsolete("Migrating to use a more descriptive endpoint that would support different types of prelogins. " + public async Task<PreloginResponseModel> PostPrelogin([FromBody] PreloginRequestModel model)
"Use prelogin/password instead. This endpoint has no EOL at the time of writing.")]
public async Task<PasswordPreloginResponseModel> PostPrelogin([FromBody] PasswordPreloginRequestModel model)
{
// Same as PostPasswordPrelogin to maintain compatibility. Do not make changes in this function body,
// only make changes in MakePasswordPreloginCall
return await MakePasswordPreloginCall(model);
}
// There are two functions done this way because the open api docs that get generated in our build pipeline
// cannot handle two of the same post attributes on the same function call. That is why there is a
// PostPrelogin and the more appropriate PostPasswordPrelogin.
[HttpPost("prelogin/password")]
public async Task<PasswordPreloginResponseModel> PostPasswordPrelogin([FromBody] PasswordPreloginRequestModel model)
{
// Same as PostPrelogin to maintain backwards compatibility. Do not make changes in this function body,
// only make changes in MakePasswordPreloginCall
return await MakePasswordPreloginCall(model);
}
private async Task<PasswordPreloginResponseModel> MakePasswordPreloginCall(PasswordPreloginRequestModel model)
{ {
var kdfInformation = await _userRepository.GetKdfInformationByEmailAsync(model.Email); var kdfInformation = await _userRepository.GetKdfInformationByEmailAsync(model.Email);
if (kdfInformation == null) if (kdfInformation == null)
{ {
kdfInformation = GetDefaultKdf(model.Email); kdfInformation = GetDefaultKdf(model.Email);
} }
return new PasswordPreloginResponseModel(kdfInformation, model.Email); return new PreloginResponseModel(kdfInformation);
} }
[HttpGet("webauthn/assertion-options")] [HttpGet("webauthn/assertion-options")]
@@ -247,17 +228,19 @@ public class AccountsController : Controller
{ {
return _defaultKdfResults[0]; return _defaultKdfResults[0];
} }
else
// Compute the HMAC hash of the email {
var hmacMessage = Encoding.UTF8.GetBytes(email.Trim().ToLowerInvariant()); // Compute the HMAC hash of the email
using var hmac = new System.Security.Cryptography.HMACSHA256(_defaultKdfHmacKey); var hmacMessage = Encoding.UTF8.GetBytes(email.Trim().ToLowerInvariant());
var hmacHash = hmac.ComputeHash(hmacMessage); using var hmac = new System.Security.Cryptography.HMACSHA256(_defaultKdfHmacKey);
// Convert the hash to a number var hmacHash = hmac.ComputeHash(hmacMessage);
var hashHex = BitConverter.ToString(hmacHash).Replace("-", string.Empty).ToLowerInvariant(); // Convert the hash to a number
var hashFirst8Bytes = hashHex.Substring(0, 16); var hashHex = BitConverter.ToString(hmacHash).Replace("-", string.Empty).ToLowerInvariant();
var hashNumber = long.Parse(hashFirst8Bytes, System.Globalization.NumberStyles.HexNumber); var hashFirst8Bytes = hashHex.Substring(0, 16);
// Find the default KDF value for this hash number var hashNumber = long.Parse(hashFirst8Bytes, System.Globalization.NumberStyles.HexNumber);
var hashIndex = (int)(Math.Abs(hashNumber) % _defaultKdfResults.Count); // Find the default KDF value for this hash number
return _defaultKdfResults[hashIndex]; var hashIndex = (int)(Math.Abs(hashNumber) % _defaultKdfResults.Count);
return _defaultKdfResults[hashIndex];
}
} }
} }

View File

@@ -5,7 +5,7 @@ using System.ComponentModel.DataAnnotations;
namespace Bit.Identity.Models.Request.Accounts; namespace Bit.Identity.Models.Request.Accounts;
public class PasswordPreloginRequestModel public class PreloginRequestModel
{ {
[Required] [Required]
[EmailAddress] [EmailAddress]

View File

@@ -1,38 +0,0 @@
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Models.Data;
namespace Bit.Identity.Models.Response.Accounts;
public class PasswordPreloginResponseModel
{
public PasswordPreloginResponseModel(UserKdfInformation kdfInformation, string? salt = null)
{
// PM-28143 Cleanup
Kdf = kdfInformation.Kdf;
KdfIterations = kdfInformation.KdfIterations;
KdfMemory = kdfInformation.KdfMemory;
KdfParallelism = kdfInformation.KdfParallelism;
// End Cleanup
KdfSettings = new KdfSettings()
{
KdfType = kdfInformation.Kdf,
Iterations = kdfInformation.KdfIterations,
Memory = kdfInformation.KdfMemory,
Parallelism = kdfInformation.KdfParallelism,
};
Salt = salt;
}
// Old Data Types
public KdfType? Kdf { get; set; } // PM-28143 Remove with cleanup
public int? KdfIterations { get; set; } // PM-28143 Remove with cleanup
public int? KdfMemory { get; set; } // PM-28143 Remove with cleanup
public int? KdfParallelism { get; set; } // PM-28143 Remove with cleanup
// New Data Types
public KdfSettings? KdfSettings { get; set; } // PM-28143 With cleanup make this not nullish
public string? Salt { get; set; } // PM-28143 With cleanup make this not nullish. Not used yet,
// just the email from the request at this time.
}

View File

@@ -0,0 +1,20 @@
using Bit.Core.Enums;
using Bit.Core.Models.Data;
namespace Bit.Identity.Models.Response.Accounts;
public class PreloginResponseModel
{
public PreloginResponseModel(UserKdfInformation kdfInformation)
{
Kdf = kdfInformation.Kdf;
KdfIterations = kdfInformation.KdfIterations;
KdfMemory = kdfInformation.KdfMemory;
KdfParallelism = kdfInformation.KdfParallelism;
}
public KdfType Kdf { get; set; }
public int KdfIterations { get; set; }
public int? KdfMemory { get; set; }
public int? KdfParallelism { get; set; }
}

View File

@@ -75,7 +75,7 @@ public class AccountsControllerTests : IDisposable
} }
[Fact] [Fact]
public async Task PostPasswordPrelogin_WhenUserExists_ShouldReturnUserKdfInfo() public async Task PostPrelogin_WhenUserExists_ShouldReturnUserKdfInfo()
{ {
var userKdfInfo = new UserKdfInformation var userKdfInfo = new UserKdfInformation
{ {
@@ -84,113 +84,30 @@ public class AccountsControllerTests : IDisposable
}; };
_userRepository.GetKdfInformationByEmailAsync(Arg.Any<string>()).Returns(userKdfInfo); _userRepository.GetKdfInformationByEmailAsync(Arg.Any<string>()).Returns(userKdfInfo);
var response = await _sut.PostPasswordPrelogin(new PasswordPreloginRequestModel { Email = "user@example.com" }); var response = await _sut.PostPrelogin(new PreloginRequestModel { Email = "user@example.com" });
Assert.Equal(userKdfInfo.Kdf, response.Kdf); Assert.Equal(userKdfInfo.Kdf, response.Kdf);
Assert.Equal(userKdfInfo.KdfIterations, response.KdfIterations); Assert.Equal(userKdfInfo.KdfIterations, response.KdfIterations);
} }
[Fact] [Fact]
public async Task PostPrelogin_And_PostPasswordPrelogin_ShouldUseSamePreloginLogic() public async Task PostPrelogin_WhenUserDoesNotExistAndNoDefaultKdfHmacKeySet_ShouldDefaultToPBKDF()
{
// Arrange: No user exists and no default HMAC key to force default path
var email = "same-user@example.com";
SetDefaultKdfHmacKey(null);
_userRepository.GetKdfInformationByEmailAsync(Arg.Any<string>()).Returns(Task.FromResult<UserKdfInformation?>(null));
// Act
var legacyResponse = await _sut.PostPrelogin(new PasswordPreloginRequestModel { Email = email });
var newResponse = await _sut.PostPasswordPrelogin(new PasswordPreloginRequestModel { Email = email });
// Assert: Both endpoints yield identical results, implying shared logic path
Assert.Equal(legacyResponse.Kdf, newResponse.Kdf);
Assert.Equal(legacyResponse.KdfIterations, newResponse.KdfIterations);
Assert.Equal(legacyResponse.KdfMemory, newResponse.KdfMemory);
Assert.Equal(legacyResponse.KdfParallelism, newResponse.KdfParallelism);
Assert.Equal(legacyResponse.Salt, newResponse.Salt);
Assert.NotNull(legacyResponse.KdfSettings);
Assert.NotNull(newResponse.KdfSettings);
Assert.Equal(legacyResponse.KdfSettings!.KdfType, newResponse.KdfSettings!.KdfType);
Assert.Equal(legacyResponse.KdfSettings!.Iterations, newResponse.KdfSettings!.Iterations);
Assert.Equal(legacyResponse.KdfSettings!.Memory, newResponse.KdfSettings!.Memory);
Assert.Equal(legacyResponse.KdfSettings!.Parallelism, newResponse.KdfSettings!.Parallelism);
// Both methods should consult the repository once each with the same email
await _userRepository.Received(2).GetKdfInformationByEmailAsync(Arg.Is<string>(e => e == email));
}
[Fact]
public async Task PostPasswordPrelogin_WhenUserExists_ReturnsNewFieldsAlignedWithLegacy_Argon2()
{
var email = "user@example.com";
var userKdfInfo = new UserKdfInformation
{
Kdf = KdfType.Argon2id,
KdfIterations = AuthConstants.ARGON2_ITERATIONS.Default,
KdfMemory = AuthConstants.ARGON2_MEMORY.Default,
KdfParallelism = AuthConstants.ARGON2_PARALLELISM.Default
};
_userRepository.GetKdfInformationByEmailAsync(Arg.Any<string>()).Returns(userKdfInfo);
var response = await _sut.PostPasswordPrelogin(new PasswordPreloginRequestModel { Email = email });
// New fields exist and match repository values
Assert.NotNull(response.KdfSettings);
Assert.Equal(userKdfInfo.Kdf, response.KdfSettings!.KdfType);
Assert.Equal(userKdfInfo.KdfIterations, response.KdfSettings!.Iterations);
Assert.Equal(userKdfInfo.KdfMemory, response.KdfSettings!.Memory);
Assert.Equal(userKdfInfo.KdfParallelism, response.KdfSettings!.Parallelism);
// New and legacy fields are aligned during migration
Assert.Equal(response.Kdf, response.KdfSettings!.KdfType);
Assert.Equal(response.KdfIterations, response.KdfSettings!.Iterations);
Assert.Equal(response.KdfMemory, response.KdfSettings!.Memory);
Assert.Equal(response.KdfParallelism, response.KdfSettings!.Parallelism);
// Salt is set to the input email during migration
Assert.Equal(email, response.Salt);
}
[Fact]
public async Task PostPasswordPrelogin_WhenUserDoesNotExistAndNoDefaultKdfHmacKeySet_ShouldDefaultToPBKDF()
{ {
SetDefaultKdfHmacKey(null); SetDefaultKdfHmacKey(null);
_userRepository.GetKdfInformationByEmailAsync(Arg.Any<string>()).Returns(Task.FromResult<UserKdfInformation?>(null)); _userRepository.GetKdfInformationByEmailAsync(Arg.Any<string>()).Returns(Task.FromResult<UserKdfInformation?>(null));
var response = await _sut.PostPasswordPrelogin(new PasswordPreloginRequestModel { Email = "user@example.com" }); var response = await _sut.PostPrelogin(new PreloginRequestModel { Email = "user@example.com" });
Assert.Equal(KdfType.PBKDF2_SHA256, response.Kdf); Assert.Equal(KdfType.PBKDF2_SHA256, response.Kdf);
Assert.Equal(AuthConstants.PBKDF2_ITERATIONS.Default, response.KdfIterations); Assert.Equal(AuthConstants.PBKDF2_ITERATIONS.Default, response.KdfIterations);
} }
[Fact]
public async Task PostPasswordPrelogin_NoUser_NoDefaultHmacKey_ReturnsAlignedNewFieldsAndSalt()
{
var email = "user@example.com";
SetDefaultKdfHmacKey(null);
_userRepository.GetKdfInformationByEmailAsync(Arg.Any<string>()).Returns(Task.FromResult<UserKdfInformation?>(null));
var response = await _sut.PostPasswordPrelogin(new PasswordPreloginRequestModel { Email = email });
// New fields exist
Assert.NotNull(response.KdfSettings);
// New and legacy fields are aligned during migration
Assert.Equal(response.Kdf, response.KdfSettings!.KdfType);
Assert.Equal(response.KdfIterations, response.KdfSettings!.Iterations);
Assert.Equal(response.KdfMemory, response.KdfSettings!.Memory);
Assert.Equal(response.KdfParallelism, response.KdfSettings!.Parallelism);
// Salt is set to the input email during migration
Assert.Equal(email, response.Salt);
}
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task PostPasswordPrelogin_WhenUserDoesNotExistAndDefaultKdfHmacKeyIsSet_ShouldComputeHmacAndReturnExpectedKdf(string email) public async Task PostPrelogin_WhenUserDoesNotExistAndDefaultKdfHmacKeyIsSet_ShouldComputeHmacAndReturnExpectedKdf(string email)
{ {
// Arrange: // Arrange:
var defaultKey = "my-secret-key"u8.ToArray(); var defaultKey = Encoding.UTF8.GetBytes("my-secret-key");
SetDefaultKdfHmacKey(defaultKey); SetDefaultKdfHmacKey(defaultKey);
_userRepository.GetKdfInformationByEmailAsync(Arg.Any<string>()).Returns(Task.FromResult<UserKdfInformation?>(null)); _userRepository.GetKdfInformationByEmailAsync(Arg.Any<string>()).Returns(Task.FromResult<UserKdfInformation?>(null));
@@ -205,7 +122,7 @@ public class AccountsControllerTests : IDisposable
var expectedKdf = defaultKdfResults[expectedIndex]; var expectedKdf = defaultKdfResults[expectedIndex];
// Act // Act
var response = await _sut.PostPasswordPrelogin(new PasswordPreloginRequestModel { Email = email }); var response = await _sut.PostPrelogin(new PreloginRequestModel { Email = email });
// Assert: Ensure the returned KDF matches the expected one from the computed hash // Assert: Ensure the returned KDF matches the expected one from the computed hash
Assert.Equal(expectedKdf.Kdf, response.Kdf); Assert.Equal(expectedKdf.Kdf, response.Kdf);
@@ -215,16 +132,6 @@ public class AccountsControllerTests : IDisposable
Assert.Equal(expectedKdf.KdfMemory, response.KdfMemory); Assert.Equal(expectedKdf.KdfMemory, response.KdfMemory);
Assert.Equal(expectedKdf.KdfParallelism, response.KdfParallelism); Assert.Equal(expectedKdf.KdfParallelism, response.KdfParallelism);
} }
// New and legacy fields are aligned during migration
Assert.NotNull(response.KdfSettings);
Assert.Equal(response.Kdf, response.KdfSettings!.KdfType);
Assert.Equal(response.KdfIterations, response.KdfSettings!.Iterations);
Assert.Equal(response.KdfMemory, response.KdfSettings!.Memory);
Assert.Equal(response.KdfParallelism, response.KdfSettings!.Parallelism);
// Salt is set to the input email during migration
Assert.Equal(email, response.Salt);
} }
[Theory] [Theory]