mirror of
https://github.com/bitwarden/server
synced 2025-12-25 12:43:14 +00: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:
@@ -0,0 +1,51 @@
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Platform.Installations;
|
||||
using Duende.IdentityServer.Models;
|
||||
using IdentityModel;
|
||||
|
||||
namespace Bit.Identity.IdentityServer.ClientProviders;
|
||||
|
||||
internal class InstallationClientProvider : IClientProvider
|
||||
{
|
||||
private readonly IInstallationRepository _installationRepository;
|
||||
|
||||
public InstallationClientProvider(IInstallationRepository installationRepository)
|
||||
{
|
||||
_installationRepository = installationRepository;
|
||||
}
|
||||
|
||||
public async Task<Client> GetAsync(string identifier)
|
||||
{
|
||||
if (!Guid.TryParse(identifier, out var installationId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var installation = await _installationRepository.GetByIdAsync(installationId);
|
||||
|
||||
if (installation == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Client
|
||||
{
|
||||
ClientId = $"installation.{installation.Id}",
|
||||
RequireClientSecret = true,
|
||||
ClientSecrets = { new Secret(installation.Key.Sha256()) },
|
||||
AllowedScopes = new[]
|
||||
{
|
||||
ApiScopes.ApiPush,
|
||||
ApiScopes.ApiLicensing,
|
||||
ApiScopes.ApiInstallation,
|
||||
},
|
||||
AllowedGrantTypes = GrantTypes.ClientCredentials,
|
||||
AccessTokenLifetime = 3600 * 24,
|
||||
Enabled = installation.Enabled,
|
||||
Claims = new List<ClientClaim>
|
||||
{
|
||||
new(JwtClaimTypes.Subject, installation.Id.ToString()),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Diagnostics;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Settings;
|
||||
using Duende.IdentityServer.Models;
|
||||
using IdentityModel;
|
||||
|
||||
namespace Bit.Identity.IdentityServer.ClientProviders;
|
||||
|
||||
internal class InternalClientProvider : IClientProvider
|
||||
{
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
public InternalClientProvider(GlobalSettings globalSettings)
|
||||
{
|
||||
// This class should not have been registered when it's not self hosted
|
||||
Debug.Assert(globalSettings.SelfHosted);
|
||||
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
public Task<Client?> GetAsync(string identifier)
|
||||
{
|
||||
return Task.FromResult<Client?>(new Client
|
||||
{
|
||||
ClientId = $"internal.{identifier}",
|
||||
RequireClientSecret = true,
|
||||
ClientSecrets = { new Secret(_globalSettings.InternalIdentityKey.Sha256()) },
|
||||
AllowedScopes = [ApiScopes.Internal],
|
||||
AllowedGrantTypes = GrantTypes.ClientCredentials,
|
||||
AccessTokenLifetime = 3600 * 24,
|
||||
Enabled = true,
|
||||
Claims =
|
||||
[
|
||||
new(JwtClaimTypes.Subject, identifier),
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Repositories;
|
||||
using Duende.IdentityServer.Models;
|
||||
using IdentityModel;
|
||||
|
||||
namespace Bit.Identity.IdentityServer.ClientProviders;
|
||||
|
||||
internal class OrganizationClientProvider : IClientProvider
|
||||
{
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
||||
|
||||
public OrganizationClientProvider(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationApiKeyRepository organizationApiKeyRepository
|
||||
)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationApiKeyRepository = organizationApiKeyRepository;
|
||||
}
|
||||
|
||||
public async Task<Client> GetAsync(string identifier)
|
||||
{
|
||||
if (!Guid.TryParse(identifier, out var organizationId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
||||
|
||||
if (organization == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var orgApiKey = (await _organizationApiKeyRepository
|
||||
.GetManyByOrganizationIdTypeAsync(organization.Id, OrganizationApiKeyType.Default))
|
||||
.First();
|
||||
|
||||
return new Client
|
||||
{
|
||||
ClientId = $"organization.{organization.Id}",
|
||||
RequireClientSecret = true,
|
||||
ClientSecrets = [new Secret(orgApiKey.ApiKey.Sha256())],
|
||||
AllowedScopes = [ApiScopes.ApiOrganization],
|
||||
AllowedGrantTypes = GrantTypes.ClientCredentials,
|
||||
AccessTokenLifetime = 3600 * 1,
|
||||
Enabled = organization.Enabled && organization.UseApi,
|
||||
Claims =
|
||||
[
|
||||
new(JwtClaimTypes.Subject, organization.Id.ToString()),
|
||||
new(Claims.Type, IdentityClientType.Organization.ToString())
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.Models.Data;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Duende.IdentityServer.Models;
|
||||
using IdentityModel;
|
||||
|
||||
namespace Bit.Identity.IdentityServer.ClientProviders;
|
||||
|
||||
internal class SecretsManagerApiKeyProvider : IClientProvider
|
||||
{
|
||||
public const string ApiKeyPrefix = "apikey";
|
||||
|
||||
private readonly IApiKeyRepository _apiKeyRepository;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
|
||||
public SecretsManagerApiKeyProvider(IApiKeyRepository apiKeyRepository, IOrganizationRepository organizationRepository)
|
||||
{
|
||||
_apiKeyRepository = apiKeyRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
}
|
||||
|
||||
public async Task<Client> GetAsync(string identifier)
|
||||
{
|
||||
if (!Guid.TryParse(identifier, out var apiKeyId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var apiKey = await _apiKeyRepository.GetDetailsByIdAsync(apiKeyId);
|
||||
|
||||
if (apiKey == null || apiKey.ExpireAt <= DateTime.UtcNow)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (apiKey)
|
||||
{
|
||||
case ServiceAccountApiKeyDetails key:
|
||||
var org = await _organizationRepository.GetByIdAsync(key.ServiceAccountOrganizationId);
|
||||
if (!org.UseSecretsManager || !org.Enabled)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
var client = new Client
|
||||
{
|
||||
ClientId = identifier,
|
||||
RequireClientSecret = true,
|
||||
ClientSecrets = { new Secret(apiKey.ClientSecretHash) },
|
||||
AllowedScopes = apiKey.GetScopes(),
|
||||
AllowedGrantTypes = GrantTypes.ClientCredentials,
|
||||
AccessTokenLifetime = 3600 * 1,
|
||||
ClientClaimsPrefix = null,
|
||||
Properties = new Dictionary<string, string> {
|
||||
{"encryptedPayload", apiKey.EncryptedPayload},
|
||||
},
|
||||
Claims = new List<ClientClaim>
|
||||
{
|
||||
new(JwtClaimTypes.Subject, apiKey.ServiceAccountId.ToString()),
|
||||
new(Claims.Type, IdentityClientType.ServiceAccount.ToString()),
|
||||
},
|
||||
};
|
||||
|
||||
switch (apiKey)
|
||||
{
|
||||
case ServiceAccountApiKeyDetails key:
|
||||
client.Claims.Add(new ClientClaim(Claims.Organization, key.ServiceAccountOrganizationId.ToString()));
|
||||
break;
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Duende.IdentityServer.Models;
|
||||
using IdentityModel;
|
||||
|
||||
namespace Bit.Identity.IdentityServer.ClientProviders;
|
||||
|
||||
public class UserClientProvider : IClientProvider
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ILicensingService _licensingService;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IProviderUserRepository _providerUserRepository;
|
||||
|
||||
public UserClientProvider(
|
||||
IUserRepository userRepository,
|
||||
ICurrentContext currentContext,
|
||||
ILicensingService licensingService,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IProviderUserRepository providerUserRepository)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_currentContext = currentContext;
|
||||
_licensingService = licensingService;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_providerUserRepository = providerUserRepository;
|
||||
}
|
||||
|
||||
public async Task<Client?> GetAsync(string identifier)
|
||||
{
|
||||
if (!Guid.TryParse(identifier, out var userId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var user = await _userRepository.GetByIdAsync(userId);
|
||||
if (user == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var claims = new Collection<ClientClaim>
|
||||
{
|
||||
new(JwtClaimTypes.Subject, user.Id.ToString()),
|
||||
new(JwtClaimTypes.AuthenticationMethod, "Application", "external"),
|
||||
new(Claims.Type, IdentityClientType.User.ToString()),
|
||||
};
|
||||
var orgs = await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id);
|
||||
var providers = await _currentContext.ProviderMembershipAsync(_providerUserRepository, user.Id);
|
||||
var isPremium = await _licensingService.ValidateUserPremiumAsync(user);
|
||||
foreach (var claim in CoreHelpers.BuildIdentityClaims(user, orgs, providers, isPremium))
|
||||
{
|
||||
var upperValue = claim.Value.ToUpperInvariant();
|
||||
var isBool = upperValue is "TRUE" or "FALSE";
|
||||
claims.Add(isBool
|
||||
? new ClientClaim(claim.Key, claim.Value, ClaimValueTypes.Boolean)
|
||||
: new ClientClaim(claim.Key, claim.Value)
|
||||
);
|
||||
}
|
||||
|
||||
return new Client
|
||||
{
|
||||
ClientId = $"user.{userId}",
|
||||
RequireClientSecret = true,
|
||||
ClientSecrets = { new Secret(user.ApiKey.Sha256()) },
|
||||
AllowedScopes = new[] { "api" },
|
||||
AllowedGrantTypes = GrantTypes.ClientCredentials,
|
||||
AccessTokenLifetime = 3600 * 1,
|
||||
ClientClaimsPrefix = null,
|
||||
Claims = claims,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user