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

Merge branch 'main' into tools/pm-21918/send-authentication-commands

This commit is contained in:
Daniel James Smith
2025-09-26 12:16:17 +02:00
committed by GitHub
112 changed files with 3404 additions and 1730 deletions

View File

@@ -29,6 +29,7 @@ using Bit.Test.Common.AutoFixture.Attributes;
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.HttpResults;
using NSubstitute;
using Xunit;
@@ -305,29 +306,14 @@ public class OrganizationUsersControllerTests
[Theory]
[BitAutoData]
public async Task DeleteAccount_WhenUserCanManageUsers_Success(
Guid orgId, Guid id, User currentUser, SutProvider<OrganizationUsersController> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(currentUser);
await sutProvider.Sut.DeleteAccount(orgId, id);
await sutProvider.GetDependency<IDeleteClaimedOrganizationUserAccountCommand>()
.Received(1)
.DeleteUserAsync(orgId, id, currentUser.Id);
}
[Theory]
[BitAutoData]
public async Task DeleteAccount_WhenCurrentUserNotFound_ThrowsUnauthorizedAccessException(
public async Task DeleteAccount_WhenCurrentUserNotFound_ReturnsUnauthorizedResult(
Guid orgId, Guid id, SutProvider<OrganizationUsersController> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs((User)null);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs((Guid?)null);
await Assert.ThrowsAsync<UnauthorizedAccessException>(() =>
sutProvider.Sut.DeleteAccount(orgId, id));
var result = await sutProvider.Sut.DeleteAccount(orgId, id);
Assert.IsType<UnauthorizedHttpResult>(result);
}
[Theory]

View File

@@ -10,6 +10,7 @@ using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Kdf;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture.Attributes;
@@ -33,6 +34,7 @@ public class AccountsControllerTests : IDisposable
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
private readonly IFeatureService _featureService;
private readonly ITwoFactorEmailService _twoFactorEmailService;
private readonly IChangeKdfCommand _changeKdfCommand;
public AccountsControllerTests()
@@ -47,7 +49,7 @@ public class AccountsControllerTests : IDisposable
_tdeOffboardingPasswordCommand = Substitute.For<ITdeOffboardingPasswordCommand>();
_featureService = Substitute.For<IFeatureService>();
_twoFactorEmailService = Substitute.For<ITwoFactorEmailService>();
_changeKdfCommand = Substitute.For<IChangeKdfCommand>();
_sut = new AccountsController(
_organizationService,
@@ -59,7 +61,8 @@ public class AccountsControllerTests : IDisposable
_tdeOffboardingPasswordCommand,
_twoFactorIsEnabledQuery,
_featureService,
_twoFactorEmailService
_twoFactorEmailService,
_changeKdfCommand
);
}
@@ -242,12 +245,18 @@ public class AccountsControllerTests : IDisposable
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
_userService.ChangePasswordAsync(user, default, default, default, default)
_userService.ChangePasswordAsync(user, Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
.Returns(Task.FromResult(IdentityResult.Success));
await _sut.PostPassword(new PasswordRequestModel());
await _sut.PostPassword(new PasswordRequestModel
{
MasterPasswordHash = "masterPasswordHash",
NewMasterPasswordHash = "newMasterPasswordHash",
MasterPasswordHint = "masterPasswordHint",
Key = "key"
});
await _userService.Received(1).ChangePasswordAsync(user, default, default, default, default);
await _userService.Received(1).ChangePasswordAsync(user, Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>());
}
[Fact]
@@ -256,7 +265,13 @@ public class AccountsControllerTests : IDisposable
ConfigureUserServiceToReturnNullPrincipal();
await Assert.ThrowsAsync<UnauthorizedAccessException>(
() => _sut.PostPassword(new PasswordRequestModel())
() => _sut.PostPassword(new PasswordRequestModel
{
MasterPasswordHash = "masterPasswordHash",
NewMasterPasswordHash = "newMasterPasswordHash",
MasterPasswordHint = "masterPasswordHint",
Key = "key"
})
);
}
@@ -265,11 +280,17 @@ public class AccountsControllerTests : IDisposable
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
_userService.ChangePasswordAsync(user, default, default, default, default)
_userService.ChangePasswordAsync(user, Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
.Returns(Task.FromResult(IdentityResult.Failed()));
await Assert.ThrowsAsync<BadRequestException>(
() => _sut.PostPassword(new PasswordRequestModel())
() => _sut.PostPassword(new PasswordRequestModel
{
MasterPasswordHash = "masterPasswordHash",
NewMasterPasswordHash = "newMasterPasswordHash",
MasterPasswordHint = "masterPasswordHint",
Key = "key"
})
);
}
@@ -593,6 +614,30 @@ public class AccountsControllerTests : IDisposable
await _twoFactorEmailService.Received(1).SendNewDeviceVerificationEmailAsync(user);
}
[Theory]
[BitAutoData]
public async Task PostKdf_WithNullAuthenticationData_ShouldFail(
User user, PasswordRequestModel model)
{
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
model.AuthenticationData = null;
// Act
await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostKdf(model));
}
[Theory]
[BitAutoData]
public async Task PostKdf_WithNullUnlockData_ShouldFail(
User user, PasswordRequestModel model)
{
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
model.UnlockData = null;
// Act
await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostKdf(model));
}
// Below are helper functions that currently belong to this
// test class, but ultimately may need to be split out into
// something greater in order to share common test steps with

View File

@@ -222,7 +222,7 @@ public class AuthRequestsControllerTests
}
[Theory, BitAutoData]
public async Task Put_ReturnsAuthRequest(
public async Task Put_WithRequestNotApproved_ReturnsAuthRequest(
SutProvider<AuthRequestsController> sutProvider,
User user,
AuthRequestUpdateRequestModel requestModel,
@@ -230,6 +230,7 @@ public class AuthRequestsControllerTests
{
// Arrange
SetBaseServiceUri(sutProvider);
requestModel.RequestApproved = false; // Not an approval, so validation should be skipped
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
@@ -248,6 +249,117 @@ public class AuthRequestsControllerTests
Assert.IsType<AuthRequestResponseModel>(result);
}
[Theory, BitAutoData]
public async Task Put_WithApprovedRequest_ValidatesAndReturnsAuthRequest(
SutProvider<AuthRequestsController> sutProvider,
User user,
AuthRequestUpdateRequestModel requestModel,
AuthRequest currentAuthRequest,
AuthRequest updatedAuthRequest,
List<PendingAuthRequestDetails> pendingRequests)
{
// Arrange
SetBaseServiceUri(sutProvider);
requestModel.RequestApproved = true; // Approval triggers validation
currentAuthRequest.RequestDeviceIdentifier = "device-identifier-123";
// Setup pending requests - make the current request the most recent for its device
var mostRecentForDevice = new PendingAuthRequestDetails(currentAuthRequest, Guid.NewGuid());
pendingRequests.Add(mostRecentForDevice);
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns(user.Id);
// Setup validation dependencies
sutProvider.GetDependency<IAuthRequestService>()
.GetAuthRequestAsync(currentAuthRequest.Id, user.Id)
.Returns(currentAuthRequest);
sutProvider.GetDependency<IAuthRequestRepository>()
.GetManyPendingAuthRequestByUserId(user.Id)
.Returns(pendingRequests);
sutProvider.GetDependency<IAuthRequestService>()
.UpdateAuthRequestAsync(currentAuthRequest.Id, user.Id, requestModel)
.Returns(updatedAuthRequest);
// Act
var result = await sutProvider.Sut
.Put(currentAuthRequest.Id, requestModel);
// Assert
Assert.NotNull(result);
Assert.IsType<AuthRequestResponseModel>(result);
}
[Theory, BitAutoData]
public async Task Put_WithApprovedRequest_CurrentAuthRequestNotFound_ThrowsNotFoundException(
SutProvider<AuthRequestsController> sutProvider,
User user,
AuthRequestUpdateRequestModel requestModel,
Guid authRequestId)
{
// Arrange
requestModel.RequestApproved = true; // Approval triggers validation
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns(user.Id);
// Current auth request not found
sutProvider.GetDependency<IAuthRequestService>()
.GetAuthRequestAsync(authRequestId, user.Id)
.Returns((AuthRequest)null);
// Act & Assert
var exception = await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.Put(authRequestId, requestModel));
}
[Theory, BitAutoData]
public async Task Put_WithApprovedRequest_NotMostRecentForDevice_ThrowsBadRequestException(
SutProvider<AuthRequestsController> sutProvider,
User user,
AuthRequestUpdateRequestModel requestModel,
AuthRequest currentAuthRequest,
List<PendingAuthRequestDetails> pendingRequests)
{
// Arrange
requestModel.RequestApproved = true; // Approval triggers validation
currentAuthRequest.RequestDeviceIdentifier = "device-identifier-123";
// Setup pending requests - make a different request the most recent for the same device
var differentAuthRequest = new AuthRequest
{
Id = Guid.NewGuid(), // Different ID than current request
RequestDeviceIdentifier = currentAuthRequest.RequestDeviceIdentifier,
UserId = user.Id,
Type = AuthRequestType.AuthenticateAndUnlock,
CreationDate = DateTime.UtcNow
};
var mostRecentForDevice = new PendingAuthRequestDetails(differentAuthRequest, Guid.NewGuid());
pendingRequests.Add(mostRecentForDevice);
sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
.Returns(user.Id);
sutProvider.GetDependency<IAuthRequestService>()
.GetAuthRequestAsync(currentAuthRequest.Id, user.Id)
.Returns(currentAuthRequest);
sutProvider.GetDependency<IAuthRequestRepository>()
.GetManyPendingAuthRequestByUserId(user.Id)
.Returns(pendingRequests);
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.Put(currentAuthRequest.Id, requestModel));
Assert.Equal("This request is no longer valid. Make sure to approve the most recent request.", exception.Message);
}
private void SetBaseServiceUri(SutProvider<AuthRequestsController> sutProvider)
{
sutProvider.GetDependency<IGlobalSettings>()

View File

@@ -18,7 +18,7 @@ public class MasterPasswordUnlockDataModelTests
[InlineData(KdfType.Argon2id, 3, 64, 4)]
public void Validate_Success(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism)
{
var model = new MasterPasswordUnlockDataModel
var model = new MasterPasswordUnlockAndAuthenticationDataModel
{
KdfType = kdfType,
KdfIterations = kdfIterations,
@@ -43,7 +43,7 @@ public class MasterPasswordUnlockDataModelTests
[InlineData((KdfType)2, 2, 64, 4)]
public void Validate_Failure(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism)
{
var model = new MasterPasswordUnlockDataModel
var model = new MasterPasswordUnlockAndAuthenticationDataModel
{
KdfType = kdfType,
KdfIterations = kdfIterations,
@@ -59,7 +59,7 @@ public class MasterPasswordUnlockDataModelTests
Assert.NotNull(result.First().ErrorMessage);
}
private static List<ValidationResult> Validate(MasterPasswordUnlockDataModel model)
private static List<ValidationResult> Validate(MasterPasswordUnlockAndAuthenticationDataModel model)
{
var results = new List<ValidationResult>();
Validator.TryValidateObject(model, new ValidationContext(model), results, true);

View File

@@ -1,65 +0,0 @@
using System.ComponentModel.DataAnnotations;
using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Core.Enums;
using Xunit;
namespace Bit.Api.Test.Models.Request.Accounts;
public class KdfRequestModelTests
{
[Theory]
[InlineData(KdfType.PBKDF2_SHA256, 1_000_000, null, null)] // Somewhere in the middle
[InlineData(KdfType.PBKDF2_SHA256, 600_000, null, null)] // Right on the lower boundary
[InlineData(KdfType.PBKDF2_SHA256, 2_000_000, null, null)] // Right on the upper boundary
[InlineData(KdfType.Argon2id, 5, 500, 8)] // Somewhere in the middle
[InlineData(KdfType.Argon2id, 2, 15, 1)] // Right on the lower boundary
[InlineData(KdfType.Argon2id, 10, 1024, 16)] // Right on the upper boundary
public void Validate_IsValid(KdfType kdfType, int? kdfIterations, int? kdfMemory, int? kdfParallelism)
{
var model = new KdfRequestModel
{
Kdf = kdfType,
KdfIterations = kdfIterations,
KdfMemory = kdfMemory,
KdfParallelism = kdfParallelism,
Key = "TEST",
NewMasterPasswordHash = "TEST",
};
var results = Validate(model);
Assert.Empty(results);
}
[Theory]
[InlineData(null, 350_000, null, null, 1)] // Although KdfType is nullable, it's marked as [Required]
[InlineData(KdfType.PBKDF2_SHA256, 500_000, null, null, 1)] // Too few iterations
[InlineData(KdfType.PBKDF2_SHA256, 2_000_001, null, null, 1)] // Too many iterations
[InlineData(KdfType.Argon2id, 0, 30, 8, 1)] // Iterations must be greater than 0
[InlineData(KdfType.Argon2id, 10, 14, 8, 1)] // Too little memory
[InlineData(KdfType.Argon2id, 10, 14, 0, 1)] // Too small of a parallelism value
[InlineData(KdfType.Argon2id, 10, 1025, 8, 1)] // Too much memory
[InlineData(KdfType.Argon2id, 10, 512, 17, 1)] // Too big of a parallelism value
public void Validate_Fails(KdfType? kdfType, int? kdfIterations, int? kdfMemory, int? kdfParallelism, int expectedFailures)
{
var model = new KdfRequestModel
{
Kdf = kdfType,
KdfIterations = kdfIterations,
KdfMemory = kdfMemory,
KdfParallelism = kdfParallelism,
Key = "TEST",
NewMasterPasswordHash = "TEST",
};
var results = Validate(model);
Assert.NotEmpty(results);
Assert.Equal(expectedFailures, results.Count);
}
public static List<ValidationResult> Validate(KdfRequestModel model)
{
var results = new List<ValidationResult>();
Validator.TryValidateObject(model, new ValidationContext(model), results);
return results;
}
}

View File

@@ -0,0 +1,36 @@
using Bit.Core.Enums;
using Bit.Core.Utilities;
using Xunit;
namespace Bit.Api.Test.Utilities;
public class KdfSettingsValidatorTests
{
[Theory]
[InlineData(KdfType.PBKDF2_SHA256, 1_000_000, null, null)] // Somewhere in the middle
[InlineData(KdfType.PBKDF2_SHA256, 600_000, null, null)] // Right on the lower boundary
[InlineData(KdfType.PBKDF2_SHA256, 2_000_000, null, null)] // Right on the upper boundary
[InlineData(KdfType.Argon2id, 5, 500, 8)] // Somewhere in the middle
[InlineData(KdfType.Argon2id, 2, 15, 1)] // Right on the lower boundary
[InlineData(KdfType.Argon2id, 10, 1024, 16)] // Right on the upper boundary
public void Validate_IsValid(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism)
{
var results = KdfSettingsValidator.Validate(kdfType, kdfIterations, kdfMemory, kdfParallelism);
Assert.Empty(results);
}
[Theory]
[InlineData(KdfType.PBKDF2_SHA256, 500_000, null, null, 1)] // Too few iterations
[InlineData(KdfType.PBKDF2_SHA256, 2_000_001, null, null, 1)] // Too many iterations
[InlineData(KdfType.Argon2id, 0, 30, 8, 1)] // Iterations must be greater than 0
[InlineData(KdfType.Argon2id, 10, 14, 8, 1)] // Too little memory
[InlineData(KdfType.Argon2id, 10, 14, 0, 1)] // Too small of a parallelism value
[InlineData(KdfType.Argon2id, 10, 1025, 8, 1)] // Too much memory
[InlineData(KdfType.Argon2id, 10, 512, 17, 1)] // Too big of a parallelism value
public void Validate_Fails(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism, int expectedFailures)
{
var results = KdfSettingsValidator.Validate(kdfType, kdfIterations, kdfMemory, kdfParallelism);
Assert.NotEmpty(results);
Assert.Equal(expectedFailures, results.Count());
}
}