diff --git a/src/Core/KeyManagement/Constants.cs b/src/Core/KeyManagement/Constants.cs index 2bc44134be..f5976752fb 100644 --- a/src/Core/KeyManagement/Constants.cs +++ b/src/Core/KeyManagement/Constants.cs @@ -2,5 +2,5 @@ public static class Constants { - public static readonly Version MinimumClientVersion = new Version("2025.11.0"); + public static readonly Version MinimumClientVersionForV2Encryption = new Version("2025.11.0"); } diff --git a/src/Core/KeyManagement/Queries/GetMinimumClientVersionForUserQuery.cs b/src/Core/KeyManagement/Queries/GetMinimumClientVersionForUserQuery.cs index fe6020ab5a..4b59a4f3d7 100644 --- a/src/Core/KeyManagement/Queries/GetMinimumClientVersionForUserQuery.cs +++ b/src/Core/KeyManagement/Queries/GetMinimumClientVersionForUserQuery.cs @@ -15,7 +15,7 @@ public class GetMinimumClientVersionForUserQuery(IIsV2EncryptionUserQuery isV2En if (await isV2EncryptionUserQuery.Run(user)) { - return Constants.MinimumClientVersion; + return Constants.MinimumClientVersionForV2Encryption; } return null; diff --git a/src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountKeysCommand.cs b/src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountKeysCommand.cs index 6e5708f667..3e392e6c37 100644 --- a/src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountKeysCommand.cs +++ b/src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountKeysCommand.cs @@ -260,6 +260,4 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand throw new InvalidOperationException("No signed security state provider for V2 user"); } } - - // Parsing moved to Bit.Core.KeyManagement.Utilities.EncryptionParsing } diff --git a/src/Identity/IdentityServer/RequestValidators/ClientVersionValidator.cs b/src/Identity/IdentityServer/RequestValidators/ClientVersionValidator.cs index 5896061e13..d8b9e5c3db 100644 --- a/src/Identity/IdentityServer/RequestValidators/ClientVersionValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/ClientVersionValidator.cs @@ -16,7 +16,7 @@ public class ClientVersionValidator( IGetMinimumClientVersionForUserQuery getMinimumClientVersionForUserQuery) : IClientVersionValidator { - private static readonly string UpgradeMessage = "Please update your app to continue using Bitwarden"; + private const string _upgradeMessage = "Please update your app to continue using Bitwarden"; public async Task ValidateAsync(User? user, CustomValidatorRequestContext requestContext) { @@ -25,10 +25,10 @@ public class ClientVersionValidator( return true; } - var clientVersion = currentContext.ClientVersion; - var minVersion = await getMinimumClientVersionForUserQuery.Run(user); + Version clientVersion = currentContext.ClientVersion; + Version? minVersion = await getMinimumClientVersionForUserQuery.Run(user); - // Fail-open if headers are missing or no restriction + // Allow through if headers are missing. if (minVersion == null) { return true; @@ -39,12 +39,12 @@ public class ClientVersionValidator( requestContext.ValidationErrorResult = new ValidationResult { Error = "invalid_client_version", - ErrorDescription = UpgradeMessage, + ErrorDescription = _upgradeMessage, IsError = true }; requestContext.CustomResponse = new Dictionary { - { "ErrorModel", new ErrorResponseModel(UpgradeMessage) } + { "ErrorModel", new ErrorResponseModel(_upgradeMessage) } }; return false; } diff --git a/test/Api.IntegrationTest/Factories/ApiApplicationFactory.cs b/test/Api.IntegrationTest/Factories/ApiApplicationFactory.cs index 93ebacf726..fe6fe7d463 100644 --- a/test/Api.IntegrationTest/Factories/ApiApplicationFactory.cs +++ b/test/Api.IntegrationTest/Factories/ApiApplicationFactory.cs @@ -68,7 +68,7 @@ public class ApiApplicationFactory : WebApplicationFactoryBase 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, }); diff --git a/test/Core.Test/KeyManagement/Queries/GetMinimumClientVersionForUserQueryTests.cs b/test/Core.Test/KeyManagement/Queries/GetMinimumClientVersionForUserQueryTests.cs index 1a04e60ca5..77410c25a0 100644 --- a/test/Core.Test/KeyManagement/Queries/GetMinimumClientVersionForUserQueryTests.cs +++ b/test/Core.Test/KeyManagement/Queries/GetMinimumClientVersionForUserQueryTests.cs @@ -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] diff --git a/test/Events.IntegrationTest/EventsApplicationFactory.cs b/test/Events.IntegrationTest/EventsApplicationFactory.cs index 7d692c442a..45e89f7dc6 100644 --- a/test/Events.IntegrationTest/EventsApplicationFactory.cs +++ b/test/Events.IntegrationTest/EventsApplicationFactory.cs @@ -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 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); diff --git a/test/Identity.IntegrationTest/Endpoints/IdentityServerSsoTests.cs b/test/Identity.IntegrationTest/Endpoints/IdentityServerSsoTests.cs index 2227428824..8846214b78 100644 --- a/test/Identity.IntegrationTest/Endpoints/IdentityServerSsoTests.cs +++ b/test/Identity.IntegrationTest/Endpoints/IdentityServerSsoTests.cs @@ -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(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(service => { - service.GetAuthorizationCodeAsync("test_code") + // Return our pre-built authorization code regardless of handle representation + service.GetAuthorizationCodeAsync(Arg.Any()) .Returns(authorizationCode); + service.RemoveAuthorizationCodeAsync(Arg.Any()) + .Returns(Task.CompletedTask); }); var user = await factory.RegisterNewIdentityFactoryUserAsync( diff --git a/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs b/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs index a04b8acf19..0e6f9931e6 100644 --- a/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs +++ b/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs @@ -387,10 +387,10 @@ public class IdentityServerTwoFactorTests : IClassFixture(); diff --git a/test/Identity.IntegrationTest/RequestValidation/VaultAccess/ResourceOwnerPasswordValidatorTests.cs b/test/Identity.IntegrationTest/RequestValidation/VaultAccess/ResourceOwnerPasswordValidatorTests.cs index 91123b3a60..90391ff699 100644 --- a/test/Identity.IntegrationTest/RequestValidation/VaultAccess/ResourceOwnerPasswordValidatorTests.cs +++ b/test/Identity.IntegrationTest/RequestValidation/VaultAccess/ResourceOwnerPasswordValidatorTests.cs @@ -613,10 +613,10 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture(); + 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); + } } diff --git a/test/IntegrationTestCommon/Factories/IdentityApplicationFactory.cs b/test/IntegrationTestCommon/Factories/IdentityApplicationFactory.cs index 3c0b551908..529f8459fd 100644 --- a/test/IntegrationTestCommon/Factories/IdentityApplicationFactory.cs +++ b/test/IntegrationTestCommon/Factories/IdentityApplicationFactory.cs @@ -75,8 +75,20 @@ public class IdentityApplicationFactory : WebApplicationFactoryBase var context = await ContextFromPasswordAsync( username, password, deviceIdentifier, clientId, deviceType, deviceName); - using var body = await AssertHelper.AssertResponseTypeIs(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(context); + var root = jsonDoc.RootElement; return (root.GetProperty("access_token").GetString(), root.GetProperty("refresh_token").GetString()); } @@ -99,7 +111,13 @@ public class IdentityApplicationFactory : WebApplicationFactoryBase { "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; } diff --git a/test/IntegrationTestCommon/Factories/WebApplicationFactoryExtensions.cs b/test/IntegrationTestCommon/Factories/WebApplicationFactoryExtensions.cs index 5613f2e683..3f5bf49dd9 100644 --- a/test/IntegrationTestCommon/Factories/WebApplicationFactoryExtensions.cs +++ b/test/IntegrationTestCommon/Factories/WebApplicationFactoryExtensions.cs @@ -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;