1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 07:36:14 -05:00

Add DynamicClientStore (#5670)

* Add DynamicClientStore

* Formatting

* Fix Debug assertion

* Make Identity internals visible to its unit tests

* Add installation client provider tests

* Add internal client provider tests

* Add DynamicClientStore tests

* Fix namespaces after merge

* Format

* Add docs and remove TODO comments

* Use preferred prefix for API keys

---------

Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com>
This commit is contained in:
Justin Baur
2025-05-30 12:58:54 -04:00
committed by GitHub
parent 63f836a73a
commit 0b2b573bd3
14 changed files with 698 additions and 294 deletions

View File

@ -9,7 +9,6 @@ using Bit.Core.Enums;
using Bit.Core.Platform.Installations;
using Bit.Core.Repositories;
using Bit.Core.Test.Auth.AutoFixture;
using Bit.Identity.IdentityServer;
using Bit.IntegrationTestCommon.Factories;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;

View File

@ -0,0 +1,75 @@
using Bit.Core.IdentityServer;
using Bit.Core.Platform.Installations;
using Bit.Identity.IdentityServer.ClientProviders;
using IdentityModel;
using NSubstitute;
using Xunit;
namespace Bit.Identity.Test.IdentityServer.ClientProviders;
public class InstallationClientProviderTests
{
private readonly IInstallationRepository _installationRepository;
private readonly InstallationClientProvider _sut;
public InstallationClientProviderTests()
{
_installationRepository = Substitute.For<IInstallationRepository>();
_sut = new InstallationClientProvider(_installationRepository);
}
[Fact]
public async Task GetAsync_NonGuidIdentifier_ReturnsNull()
{
var installationClient = await _sut.GetAsync("non-guid");
Assert.Null(installationClient);
}
[Fact]
public async Task GetAsync_NonExistingInstallationGuid_ReturnsNull()
{
var installationClient = await _sut.GetAsync(Guid.NewGuid().ToString());
Assert.Null(installationClient);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task GetAsync_ExistingClient_ReturnsClientRespectingEnabledStatus(bool enabled)
{
var installationId = Guid.NewGuid();
_installationRepository
.GetByIdAsync(installationId)
.Returns(new Installation
{
Id = installationId,
Key = "some-key",
Email = "some-email",
Enabled = enabled,
});
var installationClient = await _sut.GetAsync(installationId.ToString());
Assert.NotNull(installationClient);
Assert.Equal($"installation.{installationId}", installationClient.ClientId);
Assert.True(installationClient.RequireClientSecret);
// The usage of this secret is tested in integration tests
Assert.Single(installationClient.ClientSecrets);
Assert.Collection(
installationClient.AllowedScopes,
s => Assert.Equal(ApiScopes.ApiPush, s),
s => Assert.Equal(ApiScopes.ApiLicensing, s),
s => Assert.Equal(ApiScopes.ApiInstallation, s)
);
Assert.Equal(enabled, installationClient.Enabled);
Assert.Equal(TimeSpan.FromDays(1).TotalSeconds, installationClient.AccessTokenLifetime);
var claim = Assert.Single(installationClient.Claims);
Assert.Equal(JwtClaimTypes.Subject, claim.Type);
Assert.Equal(installationId.ToString(), claim.Value);
}
}

View File

@ -0,0 +1,43 @@
using Bit.Core.IdentityServer;
using Bit.Core.Settings;
using Bit.Identity.IdentityServer.ClientProviders;
using IdentityModel;
using Xunit;
namespace Bit.Identity.Test.IdentityServer.ClientProviders;
public class InternalClientProviderTests
{
private readonly GlobalSettings _globalSettings;
private readonly InternalClientProvider _sut;
public InternalClientProviderTests()
{
_globalSettings = new GlobalSettings
{
SelfHosted = true,
};
_sut = new InternalClientProvider(_globalSettings);
}
[Fact]
public async Task GetAsync_ReturnsInternalClient()
{
var internalClient = await _sut.GetAsync("blah");
Assert.NotNull(internalClient);
Assert.Equal($"internal.blah", internalClient.ClientId);
Assert.True(internalClient.RequireClientSecret);
var secret = Assert.Single(internalClient.ClientSecrets);
Assert.NotNull(secret);
Assert.NotNull(secret.Value);
var scope = Assert.Single(internalClient.AllowedScopes);
Assert.Equal(ApiScopes.Internal, scope);
Assert.Equal(TimeSpan.FromDays(1).TotalSeconds, internalClient.AccessTokenLifetime);
Assert.True(internalClient.Enabled);
var claim = Assert.Single(internalClient.Claims);
Assert.Equal(JwtClaimTypes.Subject, claim.Type);
Assert.Equal("blah", claim.Value);
}
}

View File

@ -0,0 +1,148 @@
using Bit.Identity.IdentityServer;
using Duende.IdentityServer.Models;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using Xunit;
namespace Bit.Identity.Test.IdentityServer;
public class DynamicClientStoreTests
{
private readonly IServiceCollection _services;
private readonly IClientProvider _apiKeyProvider;
private readonly Func<DynamicClientStore> _sutCreator;
public DynamicClientStoreTests()
{
_services = new ServiceCollection();
_apiKeyProvider = Substitute.For<IClientProvider>();
_sutCreator = () => new DynamicClientStore(
_services.BuildServiceProvider(),
_apiKeyProvider,
new StaticClientStore(new Core.Settings.GlobalSettings())
);
}
[Theory]
[InlineData("mobile")]
[InlineData("web")]
[InlineData("browser")]
[InlineData("desktop")]
[InlineData("cli")]
[InlineData("connector")]
public async Task FindClientByIdAsync_StaticClients_Works(string staticClientId)
{
var sut = _sutCreator();
var client = await sut.FindClientByIdAsync(staticClientId);
Assert.NotNull(client);
Assert.Equal(staticClientId, client.ClientId);
}
[Fact]
public async Task FindClientByIdAsync_SplitName_NoService_ReturnsNull()
{
_services.AddClientProvider<FakeClientProvider>("my-provider");
var sut = _sutCreator();
var client = await sut.FindClientByIdAsync("blah.something");
Assert.Null(client);
await _apiKeyProvider
.Received(0)
.GetAsync(Arg.Any<string>());
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task FindClientByIdAsync_SplitName_HasService_ReturnsValueFromService(bool returnNull)
{
var fakeProvider = Substitute.For<IClientProvider>();
fakeProvider
.GetAsync("something")
.Returns(returnNull ? null : new Client { ClientId = "fake" });
_services.AddKeyedSingleton("my-provider", fakeProvider);
var sut = _sutCreator();
var client = await sut.FindClientByIdAsync("my-provider.something");
if (returnNull)
{
Assert.Null(client);
}
else
{
Assert.NotNull(client);
}
await fakeProvider
.Received(1)
.GetAsync("something");
await _apiKeyProvider
.Received(0)
.GetAsync(Arg.Any<string>());
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task FindClientByIdAsync_RandomString_NotSplit_TriesApiKey(bool returnsNull)
{
_apiKeyProvider
.GetAsync("random-string")
.Returns(returnsNull ? null : new Client { ClientId = "test" });
var sut = _sutCreator();
var client = await sut.FindClientByIdAsync("random-string");
if (returnsNull)
{
Assert.Null(client);
}
else
{
Assert.NotNull(client);
}
await _apiKeyProvider
.Received(1)
.GetAsync("random-string");
}
[Theory]
[InlineData("id.")]
[InlineData("id. ")]
public async Task FindClientByIdAsync_InvalidIdentifierValue_ReturnsNull(string clientId)
{
var sut = _sutCreator();
var client = await sut.FindClientByIdAsync(clientId);
Assert.Null(client);
}
private class FakeClientProvider : IClientProvider
{
public FakeClientProvider()
{
Fake = Substitute.For<IClientProvider>();
}
public IClientProvider Fake { get; }
public Task<Client?> GetAsync(string identifier)
{
return Fake.GetAsync(identifier);
}
}
}