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:
committed by
GitHub
parent
b5554c6030
commit
3dbd17f61d
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user