From e913bead56a796abbdc85666f7c9a92968043a55 Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Wed, 26 Aug 2026 11:48:55 -0500 Subject: [PATCH] Adding fix for scim logging and comparison. --- .../Utilities/ApiKeyAuthenticationHandler.cs | 10 +- .../ApiKeyAuthenticationHandlerTests.cs | 161 ++++++++++++++++++ 2 files changed, 165 insertions(+), 6 deletions(-) create mode 100644 bitwarden_license/test/Scim.Test/Utilities/ApiKeyAuthenticationHandlerTests.cs diff --git a/bitwarden_license/src/Scim/Utilities/ApiKeyAuthenticationHandler.cs b/bitwarden_license/src/Scim/Utilities/ApiKeyAuthenticationHandler.cs index 6ebffb73cd6c..b5080960ad5c 100644 --- a/bitwarden_license/src/Scim/Utilities/ApiKeyAuthenticationHandler.cs +++ b/bitwarden_license/src/Scim/Utilities/ApiKeyAuthenticationHandler.cs @@ -2,6 +2,7 @@ using System.Text.Encodings.Web; using Bit.Core.Enums; using Bit.Core.Repositories; +using Bit.Core.Utilities; using Bit.Scim.Context; using Duende.IdentityModel; using Microsoft.AspNetCore.Authentication; @@ -12,7 +13,6 @@ namespace Bit.Scim.Utilities; public class ApiKeyAuthenticationHandler : AuthenticationHandler { - private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository; private readonly IScimContext _scimContext; @@ -21,12 +21,10 @@ public ApiKeyAuthenticationHandler( ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, - IOrganizationRepository organizationRepository, IOrganizationApiKeyRepository organizationApiKeyRepository, IScimContext scimContext) : base(options, logger, encoder, clock) { - _organizationRepository = organizationRepository; _organizationApiKeyRepository = organizationApiKeyRepository; _scimContext = scimContext; } @@ -66,13 +64,13 @@ protected override async Task HandleAuthenticateAsync() var orgApiKey = (await _organizationApiKeyRepository .GetManyByOrganizationIdTypeAsync(_scimContext.Organization.Id, OrganizationApiKeyType.Scim)) .FirstOrDefault(); - if (orgApiKey?.ApiKey != apiKey) + if (orgApiKey?.ApiKey == null || !CoreHelpers.FixedTimeEquals(orgApiKey.ApiKey, apiKey)) { - Logger.LogWarning("An API request was received with an invalid API key: {apiKey}", apiKey); + Logger.LogWarning("An API request was received with an invalid API key for organization {OrganizationId}.", _scimContext.OrganizationId); return AuthenticateResult.Fail("Invalid parameters"); } - Logger.LogInformation("Org {organizationId} authenticated", _scimContext.OrganizationId); + Logger.LogInformation("Org {OrganizationId} authenticated", _scimContext.OrganizationId); var claims = new[] { diff --git a/bitwarden_license/test/Scim.Test/Utilities/ApiKeyAuthenticationHandlerTests.cs b/bitwarden_license/test/Scim.Test/Utilities/ApiKeyAuthenticationHandlerTests.cs new file mode 100644 index 000000000000..5b74237c398d --- /dev/null +++ b/bitwarden_license/test/Scim.Test/Utilities/ApiKeyAuthenticationHandlerTests.cs @@ -0,0 +1,161 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Bit.Scim.Context; +using Bit.Scim.Utilities; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Duende.IdentityModel; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NSubstitute; +using Xunit; + +namespace Bit.Scim.Test.Utilities; + +[SutProviderCustomize] +public class ApiKeyAuthenticationHandlerTests +{ + [Theory] + [BitAutoData] + public async Task HandleAuthenticate_ValidApiKey_Succeeds(SutProvider sutProvider, Organization organization, string apiKey) + { + ArrangeScimEnabledOrganization(sutProvider, organization, apiKey); + + var result = await AuthenticateAsync(sutProvider, $"Bearer {apiKey}"); + + Assert.True(result.Succeeded); + Assert.Equal($"organization.{organization.Id}", + result.Principal.FindFirst(JwtClaimTypes.ClientId)?.Value); + Assert.Equal(organization.Id.ToString(), result.Principal.FindFirst("client_sub")?.Value); + Assert.Equal("api.scim", result.Principal.FindFirst(JwtClaimTypes.Scope)?.Value); + } + + [Theory] + [BitAutoData] + public async Task HandleAuthenticate_InvalidApiKey_FailsWithoutLoggingSubmittedKey( + SutProvider sutProvider, Organization organization, string apiKey) + { + var logger = ArrangeScimEnabledOrganization(sutProvider, organization, apiKey); + var submittedKey = apiKey + "-wrong"; + + var result = await AuthenticateAsync(sutProvider, $"Bearer {submittedKey}"); + + Assert.False(result.Succeeded); + logger.DidNotReceive().Log( + Arg.Any(), + Arg.Any(), + Arg.Is(state => state.ToString().Contains(submittedKey)), + Arg.Any(), + Arg.Any>()); + } + + [Theory] + [BitAutoData] + public async Task HandleAuthenticate_NoMatchingApiKey_Fails( + SutProvider sutProvider, Organization organization, string apiKey) + { + ArrangeScimEnabledOrganization(sutProvider, organization, apiKey); + sutProvider.GetDependency() + .GetManyByOrganizationIdTypeAsync(organization.Id, OrganizationApiKeyType.Scim) + .Returns([]); + + var result = await AuthenticateAsync(sutProvider, $"Bearer {apiKey}"); + + Assert.False(result.Succeeded); + } + + [Theory] + [BitAutoData] + public async Task HandleAuthenticate_NoOrganization_Fails(SutProvider sutProvider) + { + ArrangeLogger(sutProvider); + + sutProvider.GetDependency().OrganizationId.Returns((Guid?)null); + sutProvider.GetDependency().Organization.Returns((Organization)null); + + var result = await AuthenticateAsync(sutProvider, "Bearer anything"); + + Assert.False(result.Succeeded); + } + + [Theory] + [BitAutoData] + public async Task HandleAuthenticate_MissingAuthorizationHeader_Fails(SutProvider sutProvider, Organization organization, string apiKey) + { + ArrangeScimEnabledOrganization(sutProvider, organization, apiKey); + + var result = await AuthenticateAsync(sutProvider, authorizationHeader: null); + + Assert.False(result.Succeeded); + } + + [Theory] + [BitAutoData] + public async Task HandleAuthenticate_OrganizationCannotUseScim_Fails(SutProvider sutProvider, Organization organization, string apiKey) + { + organization.Enabled = true; + organization.UseScim = false; + + ArrangeLogger(sutProvider); + + var scimContext = sutProvider.GetDependency(); + scimContext.OrganizationId.Returns(organization.Id); + scimContext.Organization.Returns(organization); + scimContext.ScimConfiguration.Returns(new ScimConfig { Enabled = true }); + + var result = await AuthenticateAsync(sutProvider, $"Bearer {apiKey}"); + + Assert.False(result.Succeeded); + } + + private static ILogger ArrangeLogger(SutProvider sutProvider) + { + var logger = Substitute.For(); + sutProvider.GetDependency().CreateLogger(Arg.Any()).Returns(logger); + return logger; + } + + private static ILogger ArrangeScimEnabledOrganization(SutProvider sutProvider, Organization organization, string apiKey) + { + var logger = ArrangeLogger(sutProvider); + + organization.Enabled = true; + organization.UseScim = true; + + var scimContext = sutProvider.GetDependency(); + scimContext.OrganizationId.Returns(organization.Id); + scimContext.Organization.Returns(organization); + scimContext.ScimConfiguration.Returns(new ScimConfig { Enabled = true }); + + sutProvider.GetDependency() + .GetManyByOrganizationIdTypeAsync(organization.Id, OrganizationApiKeyType.Scim) + .Returns([new OrganizationApiKey { OrganizationId = organization.Id, Type = OrganizationApiKeyType.Scim, ApiKey = apiKey } + ]); + + return logger; + } + + private static async Task AuthenticateAsync(SutProvider sutProvider, string? authorizationHeader) + { + sutProvider.GetDependency>() + .Get(Arg.Any()) + .Returns(new ApiKeyAuthenticationOptions()); + + var httpContext = new DefaultHttpContext(); + if (authorizationHeader is not null) + { + httpContext.Request.Headers.Authorization = authorizationHeader; + } + + var scheme = new AuthenticationScheme( + ApiKeyAuthenticationOptions.DefaultScheme, null, typeof(ApiKeyAuthenticationHandler)); + await sutProvider.Sut.InitializeAsync(scheme, httpContext); + + return await sutProvider.Sut.AuthenticateAsync(); + } +}