1
0
mirror of https://github.com/bitwarden/server synced 2026-01-02 00:23:40 +00:00

test(auth-validator): [PM-22975] Client Version Validator - WIP changes.

This commit is contained in:
Patrick Pimentel
2025-12-02 13:46:23 -05:00
parent 8f89694f07
commit 8b8694e589
13 changed files with 65 additions and 35 deletions

View File

@@ -68,7 +68,7 @@ public class ApiApplicationFactory : WebApplicationFactoryBase<Startup>
UserAsymmetricKeys = new KeysRequestModel()
{
PublicKey = TestEncryptionConstants.PublicKey,
EncryptedPrivateKey = TestEncryptionConstants.V1EncryptedBase64 // v1-format so parsing succeeds and user is treated as v1
EncryptedPrivateKey = TestEncryptionConstants.V1EncryptedBase64
},
UserSymmetricKey = TestEncryptionConstants.V1EncryptedBase64,
});

View File

@@ -19,7 +19,7 @@ public class GetMinimumClientVersionForUserQueryTests
{
var sut = new GetMinimumClientVersionForUserQuery(new FakeIsV2Query(true));
var version = await sut.Run(new User());
Assert.Equal(Core.KeyManagement.Constants.MinimumClientVersion, version);
Assert.Equal(Core.KeyManagement.Constants.MinimumClientVersionForV2Encryption, version);
}
[Fact]

View File

@@ -3,6 +3,7 @@ using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Enums;
using Bit.IntegrationTestCommon;
using Bit.IntegrationTestCommon.Factories;
using Bit.Test.Common.Constants;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
@@ -58,10 +59,10 @@ public class EventsApplicationFactory : WebApplicationFactoryBase<Startup>
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
UserAsymmetricKeys = new KeysRequestModel()
{
PublicKey = "public_key",
EncryptedPrivateKey = "private_key"
PublicKey = TestEncryptionConstants.PublicKey,
EncryptedPrivateKey = TestEncryptionConstants.V1EncryptedBase64
},
UserSymmetricKey = "sym_key",
UserSymmetricKey = TestEncryptionConstants.V1EncryptedBase64,
});
return await _identityApplicationFactory.TokenFromPasswordAsync(email, masterPasswordHash);

View File

@@ -343,7 +343,7 @@ public class IdentityServerSsoTests
{ "code_verifier", challenge },
{ "redirect_uri", "https://localhost:8080/sso-connector.html" }
}),
http => { http.Request.Headers.Append("Bitwarden-Client-Version", "2025.11.0"); });
http => { http.Request.Headers.Append("Bitwarden-Client-Version", "2025.10.0"); });
// Assert
// If the organization has selected TrustedDeviceEncryption but the user still has their master password
@@ -415,7 +415,7 @@ public class IdentityServerSsoTests
}),
http =>
{
http.Request.Headers.Append("Bitwarden-Client-Version", "2025.11.0");
http.Request.Headers.Append("Bitwarden-Client-Version", "2025.10.0");
http.Request.Headers.Append("Accept", "application/json");
});
@@ -491,7 +491,7 @@ public class IdentityServerSsoTests
{ "code_verifier", challenge },
{ "redirect_uri", "https://localhost:8080/sso-connector.html" }
}),
http => { http.Request.Headers.Append("Bitwarden-Client-Version", "2025.11.0"); });
http => { http.Request.Headers.Append("Bitwarden-Client-Version", "2025.10.0"); });
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
using var responseBody = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
@@ -569,7 +569,7 @@ public class IdentityServerSsoTests
{ "code_verifier", challenge },
{ "redirect_uri", "https://localhost:8080/sso-connector.html" }
}),
http => { http.Request.Headers.Append("Bitwarden-Client-Version", "2025.11.0"); });
http => { http.Request.Headers.Append("Bitwarden-Client-Version", "2025.10.0"); });
// If this fails, surface detailed error information to aid debugging
if (context.Response.StatusCode != StatusCodes.Status200OK)
@@ -656,8 +656,11 @@ public class IdentityServerSsoTests
factory.SubstituteService<IAuthorizationCodeStore>(service =>
{
service.GetAuthorizationCodeAsync("test_code")
// Return our pre-built authorization code regardless of handle representation
service.GetAuthorizationCodeAsync(Arg.Any<string>())
.Returns(authorizationCode);
service.RemoveAuthorizationCodeAsync(Arg.Any<string>())
.Returns(Task.CompletedTask);
});
var user = await factory.RegisterNewIdentityFactoryUserAsync(

View File

@@ -387,10 +387,10 @@ public class IdentityServerTwoFactorTests : IClassFixture<IdentityApplicationFac
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
UserAsymmetricKeys = new KeysRequestModel()
{
PublicKey = "public_key",
EncryptedPrivateKey = "private_key"
PublicKey = Bit.Test.Common.Constants.TestEncryptionConstants.PublicKey,
EncryptedPrivateKey = Bit.Test.Common.Constants.TestEncryptionConstants.V1EncryptedBase64
},
UserSymmetricKey = "sym_key",
UserSymmetricKey = Bit.Test.Common.Constants.TestEncryptionConstants.V1EncryptedBase64,
});
Assert.NotNull(user);
@@ -441,10 +441,10 @@ public class IdentityServerTwoFactorTests : IClassFixture<IdentityApplicationFac
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
UserAsymmetricKeys = new KeysRequestModel()
{
PublicKey = "public_key",
EncryptedPrivateKey = "private_key"
PublicKey = Bit.Test.Common.Constants.TestEncryptionConstants.PublicKey,
EncryptedPrivateKey = Bit.Test.Common.Constants.TestEncryptionConstants.V1EncryptedBase64
},
UserSymmetricKey = "sym_key",
UserSymmetricKey = Bit.Test.Common.Constants.TestEncryptionConstants.V1EncryptedBase64,
});
var userService = factory.GetService<IUserService>();

View File

@@ -613,10 +613,10 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
UserAsymmetricKeys = new KeysRequestModel
{
PublicKey = "public_key",
EncryptedPrivateKey = "private_key"
PublicKey = Bit.Test.Common.Constants.TestEncryptionConstants.PublicKey,
EncryptedPrivateKey = Bit.Test.Common.Constants.TestEncryptionConstants.V1EncryptedBase64
},
UserSymmetricKey = "sym_key",
UserSymmetricKey = Bit.Test.Common.Constants.TestEncryptionConstants.V1EncryptedBase64,
});
}

View File

@@ -50,4 +50,17 @@ public class ClientVersionValidatorTests
var ok = await sut.ValidateAsync(new User(), new Bit.Identity.IdentityServer.CustomValidatorRequestContext());
Assert.True(ok);
}
[Fact]
public async Task Allows_When_ClientVersionHeaderMissing()
{
// Do not set ClientVersion on the context (remains null) and ensure we fail open
var ctx = Substitute.For<ICurrentContext>();
var minQuery = MakeMinQuery(new Version("2025.11.0"));
var sut = new ClientVersionValidator(ctx, minQuery);
var ok = await sut.ValidateAsync(new User(), new Bit.Identity.IdentityServer.CustomValidatorRequestContext());
Assert.True(ok);
}
}

View File

@@ -75,8 +75,20 @@ public class IdentityApplicationFactory : WebApplicationFactoryBase<Startup>
var context = await ContextFromPasswordAsync(
username, password, deviceIdentifier, clientId, deviceType, deviceName);
using var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
var root = body.RootElement;
// Provide clearer diagnostics on failure
if (context.Response.StatusCode != StatusCodes.Status200OK)
{
var contentType = context.Response.ContentType ?? string.Empty;
if (context.Response.Body.CanSeek)
{
context.Response.Body.Position = 0;
}
string rawBody = await new StreamReader(context.Response.Body).ReadToEndAsync();
throw new Xunit.Sdk.XunitException($"Login failed: status={context.Response.StatusCode}, contentType='{contentType}', body='{rawBody}'");
}
using var jsonDoc = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
var root = jsonDoc.RootElement;
return (root.GetProperty("access_token").GetString(), root.GetProperty("refresh_token").GetString());
}
@@ -99,7 +111,13 @@ public class IdentityApplicationFactory : WebApplicationFactoryBase<Startup>
{ "grant_type", "password" },
{ "username", username },
{ "password", password },
}));
}),
http =>
{
// Ensure JSON content negotiation for errors and set a sane client version
http.Request.Headers.Append("Accept", "application/json");
http.Request.Headers.Append("Bitwarden-Client-Version", "2025.11.0");
});
return context;
}

View File

@@ -23,9 +23,6 @@ public static class WebApplicationFactoryExtensions
// it runs after this so it will take precedence.
httpContext.Connection.RemoteIpAddress = IPAddress.Parse(FactoryConstants.WhitelistedIp);
// Ensure response body is bufferable and seekable for tests to read later
httpContext.Response.Body = new MemoryStream();
httpContext.Request.Path = new PathString(requestUri);
httpContext.Request.Method = method.Method;