1
0
mirror of https://github.com/bitwarden/server synced 2026-02-24 16:42:52 +00:00

feat(auth-validator): [Auth/PM-22975] Client Version Validator (#6588)

* feat(auth-validator): [PM-22975] Client Version Validator - Implementation.

* test(auth-validator): [PM-22975] Client Version Validator - Added tests.
This commit is contained in:
Patrick-Pimentel-Bitwarden
2026-02-23 10:00:10 -05:00
committed by GitHub
parent b5554c6030
commit 3dbd17f61d
27 changed files with 732 additions and 83 deletions

View File

@@ -58,6 +58,7 @@ public class BaseRequestValidatorTests
private readonly IAuthRequestRepository _authRequestRepository;
private readonly IMailService _mailService;
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
private readonly IClientVersionValidator _clientVersionValidator;
private readonly BaseRequestValidatorTestWrapper _sut;
@@ -82,6 +83,7 @@ public class BaseRequestValidatorTests
_authRequestRepository = Substitute.For<IAuthRequestRepository>();
_mailService = Substitute.For<IMailService>();
_userAccountKeysQuery = Substitute.For<IUserAccountKeysQuery>();
_clientVersionValidator = Substitute.For<IClientVersionValidator>();
_sut = new BaseRequestValidatorTestWrapper(
_userManager,
@@ -102,7 +104,13 @@ public class BaseRequestValidatorTests
_policyRequirementQuery,
_authRequestRepository,
_mailService,
_userAccountKeysQuery);
_userAccountKeysQuery,
_clientVersionValidator);
// Default client version validator behavior: allow to pass unless a test overrides.
_clientVersionValidator
.Validate(Arg.Any<User>(), Arg.Any<CustomValidatorRequestContext>())
.Returns(true);
}
/* Logic path
@@ -1266,6 +1274,38 @@ public class BaseRequestValidatorTests
"TwoFactorRecoveryRequested flag should be set for audit/logging");
}
[Theory]
[BitAutoData]
public async Task ValidateAsync_ClientVersionValidator_IsInvoked(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
{
// Arrange
var context = CreateContext(tokenRequest, requestContext, grantResult);
_sut.isValid = true; // ensure initial context validation passes
// Force a grant type that will evaluate SSO after client version validation
context.ValidatedTokenRequest.GrantType = "password";
// Make client version validation succeed but ensure it's invoked
_clientVersionValidator
.Validate(requestContext.User, requestContext)
.Returns(true);
// Ensure SSO requirement triggers an early stop after version validation to avoid success path setup
_policyService.AnyPoliciesApplicableToUserAsync(
Arg.Any<Guid>(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed)
.Returns(Task.FromResult(true));
// Act
await _sut.ValidateAsync(context);
// Assert
_clientVersionValidator.Received(1)
.Validate(requestContext.User, requestContext);
}
/// <summary>
/// Tests that when SSO validation returns a custom response, (e.g., with organization identifier),
/// that custom response is properly propagated to the result.

View File

@@ -0,0 +1,127 @@
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Identity.IdentityServer.RequestValidators;
using Bit.Test.Common.Constants;
using NSubstitute;
using Xunit;
namespace Bit.Identity.Test.IdentityServer.RequestValidators;
public class ClientVersionValidatorTests
{
private static ICurrentContext MakeContext(Version? version)
{
var ctx = Substitute.For<ICurrentContext>();
ctx.ClientVersion = version;
return ctx;
}
private static User MakeValidV2User()
{
return new User
{
PrivateKey = TestEncryptionConstants.V2PrivateKey,
SecurityVersion = 2
};
}
[Fact]
public void Allows_When_ClientMeetsMinimumVersion()
{
// Arrange
var sut = new ClientVersionValidator(MakeContext(new Version("2025.11.0")));
var ctx = new Bit.Identity.IdentityServer.CustomValidatorRequestContext();
var user = MakeValidV2User();
// Act
var ok = sut.Validate(user, ctx);
// Assert
Assert.True(ok);
}
[Fact]
public void Blocks_When_ClientTooOld()
{
// Arrange
var sut = new ClientVersionValidator(MakeContext(new Version("2025.10.0")));
var ctx = new Bit.Identity.IdentityServer.CustomValidatorRequestContext();
var user = MakeValidV2User();
// Act
var ok = sut.Validate(user, ctx);
// Assert
Assert.False(ok);
Assert.NotNull(ctx.ValidationErrorResult);
Assert.True(ctx.ValidationErrorResult.IsError);
Assert.Equal("invalid_client_version", ctx.ValidationErrorResult.Error);
}
[Fact]
public void Blocks_When_NullUser()
{
// Arrange
var sut = new ClientVersionValidator(MakeContext(new Version("2025.11.0")));
var ctx = new Bit.Identity.IdentityServer.CustomValidatorRequestContext();
User? user = null;
// Act
var ok = sut.Validate(user, ctx);
// Assert
Assert.False(ok);
Assert.NotNull(ctx.ValidationErrorResult);
Assert.True(ctx.ValidationErrorResult.IsError);
Assert.Equal("no_user", ctx.ValidationErrorResult.Error);
}
[Fact]
public void Allows_When_NoPrivateKey()
{
// Arrange
var sut = new ClientVersionValidator(MakeContext(new Version("2025.11.0")));
var ctx = new Bit.Identity.IdentityServer.CustomValidatorRequestContext();
var user = MakeValidV2User();
user.PrivateKey = null;
// Act
var ok = sut.Validate(user, ctx);
// Assert
Assert.True(ok);
}
[Fact]
public void Allows_When_NoSecurityVersion()
{
// Arrange
var sut = new ClientVersionValidator(MakeContext(new Version("2025.11.0")));
var ctx = new Bit.Identity.IdentityServer.CustomValidatorRequestContext();
var user = MakeValidV2User();
user.SecurityVersion = null;
// Act
var ok = sut.Validate(user, ctx);
// Assert
Assert.True(ok);
}
[Fact]
public void Blocks_When_ClientVersionHeaderMissing()
{
// Arrange
var sut = new ClientVersionValidator(MakeContext(null));
var ctx = new Bit.Identity.IdentityServer.CustomValidatorRequestContext();
var user = MakeValidV2User();
// Act
var ok = sut.Validate(user, ctx);
// Assert
Assert.False(ok);
Assert.NotNull(ctx.ValidationErrorResult);
Assert.True(ctx.ValidationErrorResult.IsError);
Assert.Equal("version_header_missing", ctx.ValidationErrorResult.Error);
}
}

View File

@@ -67,7 +67,8 @@ IBaseRequestValidatorTestWrapper
IPolicyRequirementQuery policyRequirementQuery,
IAuthRequestRepository authRequestRepository,
IMailService mailService,
IUserAccountKeysQuery userAccountKeysQuery) :
IUserAccountKeysQuery userAccountKeysQuery,
IClientVersionValidator clientVersionValidator) :
base(
userManager,
userService,
@@ -87,7 +88,8 @@ IBaseRequestValidatorTestWrapper
policyRequirementQuery,
authRequestRepository,
mailService,
userAccountKeysQuery)
userAccountKeysQuery,
clientVersionValidator)
{
}