mirror of
https://github.com/bitwarden/server.git
synced 2024-11-22 12:15:36 +01:00
Added the ability to create a JWT on an organization license that contains all license properties as claims
This commit is contained in:
parent
b07df10335
commit
02fd8b0f3e
@ -1,5 +1,6 @@
|
||||
using Bit.Core.Billing.Caches;
|
||||
using Bit.Core.Billing.Caches.Implementations;
|
||||
using Bit.Core.Billing.Licenses.Extensions;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Implementations;
|
||||
|
||||
@ -15,5 +16,6 @@ public static class ServiceCollectionExtensions
|
||||
services.AddTransient<IPremiumUserBillingService, PremiumUserBillingService>();
|
||||
services.AddTransient<ISetupIntentCache, SetupIntentDistributedCache>();
|
||||
services.AddTransient<ISubscriberService, SubscriberService>();
|
||||
services.AddLicenseServices();
|
||||
}
|
||||
}
|
||||
|
78
src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs
Normal file
78
src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs
Normal file
@ -0,0 +1,78 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
|
||||
namespace Bit.Core.Billing.Licenses.Extensions;
|
||||
|
||||
public static class LicenseExtensions
|
||||
{
|
||||
|
||||
public static DateTime CalculateFreshExpirationDate(this Organization org, SubscriptionInfo subscriptionInfo)
|
||||
{
|
||||
if (subscriptionInfo?.Subscription == null)
|
||||
{
|
||||
if (org.PlanType == PlanType.Custom && org.ExpirationDate.HasValue)
|
||||
{
|
||||
return org.ExpirationDate.Value;
|
||||
}
|
||||
|
||||
return DateTime.UtcNow.AddDays(7);
|
||||
}
|
||||
|
||||
var subscription = subscriptionInfo.Subscription;
|
||||
|
||||
if (subscription.TrialEndDate > DateTime.UtcNow)
|
||||
{
|
||||
return subscription.TrialEndDate.Value;
|
||||
}
|
||||
|
||||
if (org.ExpirationDate.HasValue && org.ExpirationDate.Value < DateTime.UtcNow)
|
||||
{
|
||||
return org.ExpirationDate.Value;
|
||||
}
|
||||
|
||||
if (subscription.PeriodEndDate.HasValue && subscription.PeriodDuration > TimeSpan.FromDays(180))
|
||||
{
|
||||
return subscription.PeriodEndDate
|
||||
.Value
|
||||
.AddDays(Bit.Core.Constants.OrganizationSelfHostSubscriptionGracePeriodDays);
|
||||
}
|
||||
|
||||
return org.ExpirationDate?.AddMonths(11) ?? DateTime.UtcNow.AddYears(1);
|
||||
}
|
||||
|
||||
public static DateTime CalculateFreshRefreshDate(this Organization org, SubscriptionInfo subscriptionInfo, DateTime expirationDate)
|
||||
{
|
||||
if (subscriptionInfo?.Subscription == null ||
|
||||
subscriptionInfo.Subscription.TrialEndDate > DateTime.UtcNow ||
|
||||
org.ExpirationDate < DateTime.UtcNow)
|
||||
{
|
||||
return expirationDate;
|
||||
}
|
||||
|
||||
return subscriptionInfo.Subscription.PeriodDuration > TimeSpan.FromDays(180) ||
|
||||
DateTime.UtcNow - expirationDate > TimeSpan.FromDays(30)
|
||||
? DateTime.UtcNow.AddDays(30)
|
||||
: expirationDate;
|
||||
}
|
||||
|
||||
public static DateTime CalculateFreshExpirationDateWithoutGracePeriod(this Organization org, SubscriptionInfo subscriptionInfo, DateTime expirationDate)
|
||||
{
|
||||
if (subscriptionInfo?.Subscription is null)
|
||||
{
|
||||
return expirationDate;
|
||||
}
|
||||
|
||||
var subscription = subscriptionInfo.Subscription;
|
||||
|
||||
if (subscription.TrialEndDate <= DateTime.UtcNow &&
|
||||
org.ExpirationDate >= DateTime.UtcNow &&
|
||||
subscription.PeriodEndDate.HasValue &&
|
||||
subscription.PeriodDuration > TimeSpan.FromDays(180))
|
||||
{
|
||||
return subscription.PeriodEndDate.Value;
|
||||
}
|
||||
|
||||
return expirationDate;
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Licenses.Services;
|
||||
using Bit.Core.Billing.Licenses.Services.Implementations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.Core.Billing.Licenses.Extensions;
|
||||
|
||||
public static class LicenseServiceCollectionExtensions
|
||||
{
|
||||
public static void AddLicenseServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddTransient<ILicenseClaimsFactory<Organization>, OrganizationLicenseClaimsFactory>();
|
||||
}
|
||||
}
|
10
src/Core/Billing/Licenses/Models/LicenseContext.cs
Normal file
10
src/Core/Billing/Licenses/Models/LicenseContext.cs
Normal file
@ -0,0 +1,10 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Models.Business;
|
||||
|
||||
namespace Bit.Core.Billing.Licenses.Models;
|
||||
|
||||
public class LicenseContext
|
||||
{
|
||||
public Guid? InstallationId { get; init; }
|
||||
public required SubscriptionInfo SubscriptionInfo { get; init; }
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.Billing.Licenses.Models;
|
||||
|
||||
namespace Bit.Core.Billing.Licenses.Services;
|
||||
|
||||
public interface ILicenseClaimsFactory<in T>
|
||||
{
|
||||
Task<List<Claim>> GenerateClaims(T entity, LicenseContext licenseContext);
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
using System.Globalization;
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Licenses.Extensions;
|
||||
using Bit.Core.Billing.Licenses.Models;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
|
||||
namespace Bit.Core.Billing.Licenses.Services.Implementations;
|
||||
|
||||
public class OrganizationLicenseClaimsFactory : ILicenseClaimsFactory<Organization>
|
||||
{
|
||||
public Task<List<Claim>> GenerateClaims(Organization entity, LicenseContext licenseContext)
|
||||
{
|
||||
var subscriptionInfo = licenseContext.SubscriptionInfo;
|
||||
var expires = entity.CalculateFreshExpirationDate(subscriptionInfo);
|
||||
var refresh = entity.CalculateFreshRefreshDate(subscriptionInfo, expires);
|
||||
var expirationWithoutGracePeriod = entity.CalculateFreshExpirationDateWithoutGracePeriod(subscriptionInfo, expires);
|
||||
var trial = IsTrialing(entity, subscriptionInfo);
|
||||
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(nameof(OrganizationLicense.LicenseType), LicenseType.Organization.ToString()),
|
||||
new(nameof(OrganizationLicense.InstallationId), licenseContext.InstallationId.ToString()),
|
||||
new(nameof(OrganizationLicense.Id), entity.Id.ToString()),
|
||||
new(nameof(OrganizationLicense.Name), entity.Name),
|
||||
new(nameof(OrganizationLicense.BillingEmail), entity.BillingEmail),
|
||||
new(nameof(OrganizationLicense.Enabled), entity.Enabled.ToString()),
|
||||
new(nameof(OrganizationLicense.Plan), entity.Plan),
|
||||
new(nameof(OrganizationLicense.PlanType), entity.PlanType.ToString()),
|
||||
new(nameof(OrganizationLicense.Seats), entity.Seats.ToString()),
|
||||
new(nameof(OrganizationLicense.MaxCollections), entity.MaxCollections.ToString()),
|
||||
new(nameof(OrganizationLicense.UsePolicies), entity.UsePolicies.ToString()),
|
||||
new(nameof(OrganizationLicense.UseSso), entity.UseSso.ToString()),
|
||||
new(nameof(OrganizationLicense.UseKeyConnector), entity.UseKeyConnector.ToString()),
|
||||
new(nameof(OrganizationLicense.UseScim), entity.UseScim.ToString()),
|
||||
new(nameof(OrganizationLicense.UseGroups), entity.UseGroups.ToString()),
|
||||
new(nameof(OrganizationLicense.UseEvents), entity.UseEvents.ToString()),
|
||||
new(nameof(OrganizationLicense.UseDirectory), entity.UseDirectory.ToString()),
|
||||
new(nameof(OrganizationLicense.UseTotp), entity.UseTotp.ToString()),
|
||||
new(nameof(OrganizationLicense.Use2fa), entity.Use2fa.ToString()),
|
||||
new(nameof(OrganizationLicense.UseApi), entity.UseApi.ToString()),
|
||||
new(nameof(OrganizationLicense.UseResetPassword), entity.UseResetPassword.ToString()),
|
||||
new(nameof(OrganizationLicense.MaxStorageGb), entity.MaxStorageGb.ToString()),
|
||||
new(nameof(OrganizationLicense.SelfHost), entity.SelfHost.ToString()),
|
||||
new(nameof(OrganizationLicense.UsersGetPremium), entity.UsersGetPremium.ToString()),
|
||||
new(nameof(OrganizationLicense.UseCustomPermissions), entity.UseCustomPermissions.ToString()),
|
||||
new(nameof(OrganizationLicense.Issued), DateTime.UtcNow.ToString(CultureInfo.InvariantCulture)),
|
||||
new(nameof(OrganizationLicense.UsePasswordManager), entity.UsePasswordManager.ToString()),
|
||||
new(nameof(OrganizationLicense.UseSecretsManager), entity.UseSecretsManager.ToString()),
|
||||
new(nameof(OrganizationLicense.SmSeats), entity.SmSeats.ToString()),
|
||||
new(nameof(OrganizationLicense.SmServiceAccounts), entity.SmServiceAccounts.ToString()),
|
||||
new(nameof(OrganizationLicense.LimitCollectionCreationDeletion), entity.LimitCollectionCreationDeletion.ToString()),
|
||||
new(nameof(OrganizationLicense.AllowAdminAccessToAllCollectionItems), entity.AllowAdminAccessToAllCollectionItems.ToString()),
|
||||
new(nameof(OrganizationLicense.Expires), expires.ToString(CultureInfo.InvariantCulture)),
|
||||
new(nameof(OrganizationLicense.Refresh), refresh.ToString(CultureInfo.InvariantCulture)),
|
||||
new(nameof(OrganizationLicense.ExpirationWithoutGracePeriod), expirationWithoutGracePeriod.ToString(CultureInfo.InvariantCulture)),
|
||||
new(nameof(OrganizationLicense.Trial), trial.ToString()),
|
||||
};
|
||||
|
||||
if (entity.LicenseKey is not null)
|
||||
{
|
||||
claims.Add(new Claim(nameof(OrganizationLicense.LicenseKey), entity.LicenseKey));
|
||||
}
|
||||
|
||||
if (entity.BusinessName is not null)
|
||||
{
|
||||
claims.Add(new Claim(nameof(OrganizationLicense.BusinessName), entity.BusinessName));
|
||||
}
|
||||
|
||||
return Task.FromResult(claims);
|
||||
}
|
||||
|
||||
private static bool IsTrialing(Organization org, SubscriptionInfo subscriptionInfo) =>
|
||||
subscriptionInfo?.Subscription is null
|
||||
? org.PlanType != PlanType.Custom || !org.ExpirationDate.HasValue
|
||||
: subscriptionInfo.Subscription.TrialEndDate > DateTime.UtcNow;
|
||||
}
|
@ -151,6 +151,7 @@ public static class FeatureFlagKeys
|
||||
public const string GeneratorToolsModernization = "generator-tools-modernization";
|
||||
public const string NewDeviceVerification = "new-device-verification";
|
||||
public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application";
|
||||
public const string SelfHostLicenseRefactor = "pm-11516-self-host-license-refactor";
|
||||
|
||||
public static List<string> GetAllKeys()
|
||||
{
|
||||
|
@ -151,6 +151,7 @@ public class OrganizationLicense : ILicense
|
||||
public LicenseType? LicenseType { get; set; }
|
||||
public string Hash { get; set; }
|
||||
public string Signature { get; set; }
|
||||
public string Token { get; set; }
|
||||
[JsonIgnore] public byte[] SignatureBytes => Convert.FromBase64String(Signature);
|
||||
|
||||
/// <summary>
|
||||
@ -176,6 +177,7 @@ public class OrganizationLicense : ILicense
|
||||
!p.Name.Equals(nameof(Signature)) &&
|
||||
!p.Name.Equals(nameof(SignatureBytes)) &&
|
||||
!p.Name.Equals(nameof(LicenseType)) &&
|
||||
!p.Name.Equals(nameof(Token)) &&
|
||||
// UsersGetPremium was added in Version 2
|
||||
(Version >= 2 || !p.Name.Equals(nameof(UsersGetPremium))) &&
|
||||
// UseEvents was added in Version 3
|
||||
|
@ -33,6 +33,10 @@ public class CloudGetOrganizationLicenseQuery : ICloudGetOrganizationLicenseQuer
|
||||
}
|
||||
|
||||
var subscriptionInfo = await _paymentService.GetSubscriptionAsync(organization);
|
||||
return new OrganizationLicense(organization, subscriptionInfo, installationId, _licensingService, version);
|
||||
|
||||
return new OrganizationLicense(organization, subscriptionInfo, installationId, _licensingService, version)
|
||||
{
|
||||
Token = await _licensingService.CreateOrganizationTokenAsync(organization, installationId, subscriptionInfo)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -14,4 +14,8 @@ public interface ILicensingService
|
||||
Task<OrganizationLicense> ReadOrganizationLicenseAsync(Organization organization);
|
||||
Task<OrganizationLicense> ReadOrganizationLicenseAsync(Guid organizationId);
|
||||
|
||||
Task<string> CreateOrganizationTokenAsync(
|
||||
Organization organization,
|
||||
Guid installationId,
|
||||
SubscriptionInfo subscriptionInfo);
|
||||
}
|
||||
|
@ -1,15 +1,22 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Licenses.Extensions;
|
||||
using Bit.Core.Billing.Licenses.Models;
|
||||
using Bit.Core.Billing.Licenses.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
|
||||
@ -22,6 +29,8 @@ public class LicensingService : ILicensingService
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly ILogger<LicensingService> _logger;
|
||||
private readonly ILicenseClaimsFactory<Organization> _organizationLicenseClaimsFactory;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
private IDictionary<Guid, DateTime> _userCheckCache = new Dictionary<Guid, DateTime>();
|
||||
|
||||
@ -32,7 +41,9 @@ public class LicensingService : ILicensingService
|
||||
IMailService mailService,
|
||||
IWebHostEnvironment environment,
|
||||
ILogger<LicensingService> logger,
|
||||
IGlobalSettings globalSettings)
|
||||
IGlobalSettings globalSettings,
|
||||
ILicenseClaimsFactory<Organization> organizationLicenseClaimsFactory,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
@ -40,6 +51,8 @@ public class LicensingService : ILicensingService
|
||||
_mailService = mailService;
|
||||
_logger = logger;
|
||||
_globalSettings = globalSettings;
|
||||
_organizationLicenseClaimsFactory = organizationLicenseClaimsFactory;
|
||||
_featureService = featureService;
|
||||
|
||||
var certThumbprint = environment.IsDevelopment() ?
|
||||
"207E64A231E8AA32AAF68A61037C075EBEBD553F" :
|
||||
@ -272,4 +285,46 @@ public class LicensingService : ILicensingService
|
||||
using var fs = File.OpenRead(filePath);
|
||||
return await JsonSerializer.DeserializeAsync<OrganizationLicense>(fs);
|
||||
}
|
||||
|
||||
public async Task<string> CreateOrganizationTokenAsync(Organization organization, Guid installationId, SubscriptionInfo subscriptionInfo)
|
||||
{
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.SelfHostLicenseRefactor))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var licenseContext = new LicenseContext
|
||||
{
|
||||
InstallationId = installationId,
|
||||
SubscriptionInfo = subscriptionInfo,
|
||||
};
|
||||
|
||||
var claims = await _organizationLicenseClaimsFactory.GenerateClaims(organization, licenseContext);
|
||||
var audience = organization.Id.ToString();
|
||||
var expires = organization.CalculateFreshExpirationDate(subscriptionInfo);
|
||||
return GenerateToken(claims, audience, expires);
|
||||
}
|
||||
|
||||
private string GenerateToken(List<Claim> claims, string audience, DateTime expires)
|
||||
{
|
||||
if (claims.All(claim => claim.Type != JwtClaimTypes.JwtId))
|
||||
{
|
||||
claims.Add(new Claim(JwtClaimTypes.JwtId, Guid.NewGuid().ToString()));
|
||||
}
|
||||
|
||||
var securityKey = new RsaSecurityKey(_certificate.GetRSAPrivateKey());
|
||||
var tokenDescriptor = new SecurityTokenDescriptor
|
||||
{
|
||||
Subject = new ClaimsIdentity(claims),
|
||||
Issuer = "bitwarden",
|
||||
Audience = audience,
|
||||
NotBefore = DateTime.UtcNow,
|
||||
Expires = expires,
|
||||
SigningCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.RsaSha256Signature)
|
||||
};
|
||||
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var token = tokenHandler.CreateToken(tokenDescriptor);
|
||||
return tokenHandler.WriteToken(token);
|
||||
}
|
||||
}
|
||||
|
@ -53,4 +53,9 @@ public class NoopLicensingService : ILicensingService
|
||||
{
|
||||
return Task.FromResult<OrganizationLicense>(null);
|
||||
}
|
||||
|
||||
public Task<string> CreateOrganizationTokenAsync(Organization organization, Guid installationId, SubscriptionInfo subscriptionInfo)
|
||||
{
|
||||
return Task.FromResult<string>(null);
|
||||
}
|
||||
}
|
||||
|
@ -226,7 +226,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddScoped<IPaymentHistoryService, PaymentHistoryService>();
|
||||
services.AddSingleton<IStripeSyncService, StripeSyncService>();
|
||||
services.AddSingleton<IMailService, HandlebarsMailService>();
|
||||
services.AddSingleton<ILicensingService, LicensingService>();
|
||||
services.AddScoped<ILicensingService, LicensingService>();
|
||||
services.AddSingleton<ILookupClient>(_ =>
|
||||
{
|
||||
var options = new LookupClientOptions { Timeout = TimeSpan.FromSeconds(15), UseTcpOnly = true };
|
||||
|
@ -80,7 +80,7 @@ public class UpdateOrganizationLicenseCommandTests
|
||||
.ReplaceAndUpdateCacheAsync(Arg.Is<Organization>(
|
||||
org => AssertPropertyEqual(license, org,
|
||||
"Id", "MaxStorageGb", "Issued", "Refresh", "Version", "Trial", "LicenseType",
|
||||
"Hash", "Signature", "SignatureBytes", "InstallationId", "Expires", "ExpirationWithoutGracePeriod") &&
|
||||
"Hash", "Signature", "SignatureBytes", "InstallationId", "Expires", "ExpirationWithoutGracePeriod", "Token") &&
|
||||
// Same property but different name, use explicit mapping
|
||||
org.ExpirationDate == license.Expires));
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user