mirror of
https://github.com/bitwarden/server
synced 2026-01-06 18:43:36 +00:00
[PM-11516] Initial license file refactor (#5002)
* Added the ability to create a JWT on an organization license that contains all license properties as claims * Added the ability to create a JWT on a user license that contains all license properties as claims * Added ability to consume JWT licenses * Resolved generic type issues when getting claim value * Now validating the jwt signature, exp, and iat * Moved creation of ClaimsPrincipal outside of licenses given dependecy on cert * Ran dotnet format. Resolved identity error * Updated claim types to use string constants * Updated jwt expires to be one year * Fixed bug requiring email verification to be on the token * dotnet format * Patch build process --------- Co-authored-by: Matt Bishop <mbishop@bitwarden.com>
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
151
src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs
Normal file
151
src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using System.Security.Claims;
|
||||
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;
|
||||
}
|
||||
|
||||
public static T GetValue<T>(this ClaimsPrincipal principal, string claimType)
|
||||
{
|
||||
var claim = principal.FindFirst(claimType);
|
||||
|
||||
if (claim is null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
// Handle Guid
|
||||
if (typeof(T) == typeof(Guid))
|
||||
{
|
||||
return Guid.TryParse(claim.Value, out var guid)
|
||||
? (T)(object)guid
|
||||
: default;
|
||||
}
|
||||
|
||||
// Handle DateTime
|
||||
if (typeof(T) == typeof(DateTime))
|
||||
{
|
||||
return DateTime.TryParse(claim.Value, out var dateTime)
|
||||
? (T)(object)dateTime
|
||||
: default;
|
||||
}
|
||||
|
||||
// Handle TimeSpan
|
||||
if (typeof(T) == typeof(TimeSpan))
|
||||
{
|
||||
return TimeSpan.TryParse(claim.Value, out var timeSpan)
|
||||
? (T)(object)timeSpan
|
||||
: default;
|
||||
}
|
||||
|
||||
// Check for Nullable Types
|
||||
var underlyingType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
||||
|
||||
// Handle Enums
|
||||
if (underlyingType.IsEnum)
|
||||
{
|
||||
if (Enum.TryParse(underlyingType, claim.Value, true, out var enumValue))
|
||||
{
|
||||
return (T)enumValue; // Cast back to T
|
||||
}
|
||||
|
||||
return default; // Return default value for non-nullable enums or null for nullable enums
|
||||
}
|
||||
|
||||
// Handle other Nullable Types (e.g., int?, bool?)
|
||||
if (underlyingType == typeof(int))
|
||||
{
|
||||
return int.TryParse(claim.Value, out var intValue)
|
||||
? (T)(object)intValue
|
||||
: default;
|
||||
}
|
||||
|
||||
if (underlyingType == typeof(bool))
|
||||
{
|
||||
return bool.TryParse(claim.Value, out var boolValue)
|
||||
? (T)(object)boolValue
|
||||
: default;
|
||||
}
|
||||
|
||||
if (underlyingType == typeof(double))
|
||||
{
|
||||
return double.TryParse(claim.Value, out var doubleValue)
|
||||
? (T)(object)doubleValue
|
||||
: default;
|
||||
}
|
||||
|
||||
// Fallback to Convert.ChangeType for other types including strings
|
||||
return (T)Convert.ChangeType(claim.Value, underlyingType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Licenses.Services;
|
||||
using Bit.Core.Billing.Licenses.Services.Implementations;
|
||||
using Bit.Core.Entities;
|
||||
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>();
|
||||
services.AddTransient<ILicenseClaimsFactory<User>, UserLicenseClaimsFactory>();
|
||||
}
|
||||
}
|
||||
58
src/Core/Billing/Licenses/LicenseConstants.cs
Normal file
58
src/Core/Billing/Licenses/LicenseConstants.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
namespace Bit.Core.Billing.Licenses;
|
||||
|
||||
public static class OrganizationLicenseConstants
|
||||
{
|
||||
public const string LicenseType = nameof(LicenseType);
|
||||
public const string LicenseKey = nameof(LicenseKey);
|
||||
public const string InstallationId = nameof(InstallationId);
|
||||
public const string Id = nameof(Id);
|
||||
public const string Name = nameof(Name);
|
||||
public const string BusinessName = nameof(BusinessName);
|
||||
public const string BillingEmail = nameof(BillingEmail);
|
||||
public const string Enabled = nameof(Enabled);
|
||||
public const string Plan = nameof(Plan);
|
||||
public const string PlanType = nameof(PlanType);
|
||||
public const string Seats = nameof(Seats);
|
||||
public const string MaxCollections = nameof(MaxCollections);
|
||||
public const string UsePolicies = nameof(UsePolicies);
|
||||
public const string UseSso = nameof(UseSso);
|
||||
public const string UseKeyConnector = nameof(UseKeyConnector);
|
||||
public const string UseScim = nameof(UseScim);
|
||||
public const string UseGroups = nameof(UseGroups);
|
||||
public const string UseEvents = nameof(UseEvents);
|
||||
public const string UseDirectory = nameof(UseDirectory);
|
||||
public const string UseTotp = nameof(UseTotp);
|
||||
public const string Use2fa = nameof(Use2fa);
|
||||
public const string UseApi = nameof(UseApi);
|
||||
public const string UseResetPassword = nameof(UseResetPassword);
|
||||
public const string MaxStorageGb = nameof(MaxStorageGb);
|
||||
public const string SelfHost = nameof(SelfHost);
|
||||
public const string UsersGetPremium = nameof(UsersGetPremium);
|
||||
public const string UseCustomPermissions = nameof(UseCustomPermissions);
|
||||
public const string Issued = nameof(Issued);
|
||||
public const string UsePasswordManager = nameof(UsePasswordManager);
|
||||
public const string UseSecretsManager = nameof(UseSecretsManager);
|
||||
public const string SmSeats = nameof(SmSeats);
|
||||
public const string SmServiceAccounts = nameof(SmServiceAccounts);
|
||||
public const string LimitCollectionCreationDeletion = nameof(LimitCollectionCreationDeletion);
|
||||
public const string AllowAdminAccessToAllCollectionItems = nameof(AllowAdminAccessToAllCollectionItems);
|
||||
public const string Expires = nameof(Expires);
|
||||
public const string Refresh = nameof(Refresh);
|
||||
public const string ExpirationWithoutGracePeriod = nameof(ExpirationWithoutGracePeriod);
|
||||
public const string Trial = nameof(Trial);
|
||||
}
|
||||
|
||||
public static class UserLicenseConstants
|
||||
{
|
||||
public const string LicenseType = nameof(LicenseType);
|
||||
public const string LicenseKey = nameof(LicenseKey);
|
||||
public const string Id = nameof(Id);
|
||||
public const string Name = nameof(Name);
|
||||
public const string Email = nameof(Email);
|
||||
public const string Premium = nameof(Premium);
|
||||
public const string MaxStorageGb = nameof(MaxStorageGb);
|
||||
public const string Issued = nameof(Issued);
|
||||
public const string Expires = nameof(Expires);
|
||||
public const string Refresh = nameof(Refresh);
|
||||
public const string Trial = nameof(Trial);
|
||||
}
|
||||
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,75 @@
|
||||
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(OrganizationLicenseConstants.LicenseType), LicenseType.Organization.ToString()),
|
||||
new Claim(nameof(OrganizationLicenseConstants.LicenseKey), entity.LicenseKey),
|
||||
new(nameof(OrganizationLicenseConstants.InstallationId), licenseContext.InstallationId.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.Id), entity.Id.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.Name), entity.Name),
|
||||
new(nameof(OrganizationLicenseConstants.BillingEmail), entity.BillingEmail),
|
||||
new(nameof(OrganizationLicenseConstants.Enabled), entity.Enabled.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.Plan), entity.Plan),
|
||||
new(nameof(OrganizationLicenseConstants.PlanType), entity.PlanType.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.Seats), entity.Seats.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.MaxCollections), entity.MaxCollections.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UsePolicies), entity.UsePolicies.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseSso), entity.UseSso.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseKeyConnector), entity.UseKeyConnector.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseScim), entity.UseScim.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseGroups), entity.UseGroups.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseEvents), entity.UseEvents.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseDirectory), entity.UseDirectory.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseTotp), entity.UseTotp.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.Use2fa), entity.Use2fa.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseApi), entity.UseApi.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseResetPassword), entity.UseResetPassword.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.MaxStorageGb), entity.MaxStorageGb.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.SelfHost), entity.SelfHost.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UsersGetPremium), entity.UsersGetPremium.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseCustomPermissions), entity.UseCustomPermissions.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.Issued), DateTime.UtcNow.ToString(CultureInfo.InvariantCulture)),
|
||||
new(nameof(OrganizationLicenseConstants.UsePasswordManager), entity.UsePasswordManager.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseSecretsManager), entity.UseSecretsManager.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.SmSeats), entity.SmSeats.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.SmServiceAccounts), entity.SmServiceAccounts.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.LimitCollectionCreationDeletion), entity.LimitCollectionCreationDeletion.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.AllowAdminAccessToAllCollectionItems), entity.AllowAdminAccessToAllCollectionItems.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.Expires), expires.ToString(CultureInfo.InvariantCulture)),
|
||||
new(nameof(OrganizationLicenseConstants.Refresh), refresh.ToString(CultureInfo.InvariantCulture)),
|
||||
new(nameof(OrganizationLicenseConstants.ExpirationWithoutGracePeriod), expirationWithoutGracePeriod.ToString(CultureInfo.InvariantCulture)),
|
||||
new(nameof(OrganizationLicenseConstants.Trial), trial.ToString()),
|
||||
};
|
||||
|
||||
if (entity.BusinessName is not null)
|
||||
{
|
||||
claims.Add(new Claim(nameof(OrganizationLicenseConstants.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;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System.Globalization;
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.Billing.Licenses.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Billing.Licenses.Services.Implementations;
|
||||
|
||||
public class UserLicenseClaimsFactory : ILicenseClaimsFactory<User>
|
||||
{
|
||||
public Task<List<Claim>> GenerateClaims(User entity, LicenseContext licenseContext)
|
||||
{
|
||||
var subscriptionInfo = licenseContext.SubscriptionInfo;
|
||||
|
||||
var expires = subscriptionInfo.UpcomingInvoice?.Date?.AddDays(7) ?? entity.PremiumExpirationDate?.AddDays(7);
|
||||
var refresh = subscriptionInfo.UpcomingInvoice?.Date ?? entity.PremiumExpirationDate;
|
||||
var trial = (subscriptionInfo.Subscription?.TrialEndDate.HasValue ?? false) &&
|
||||
subscriptionInfo.Subscription.TrialEndDate.Value > DateTime.UtcNow;
|
||||
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(nameof(UserLicenseConstants.LicenseType), LicenseType.User.ToString()),
|
||||
new(nameof(UserLicenseConstants.LicenseKey), entity.LicenseKey),
|
||||
new(nameof(UserLicenseConstants.Id), entity.Id.ToString()),
|
||||
new(nameof(UserLicenseConstants.Name), entity.Name),
|
||||
new(nameof(UserLicenseConstants.Email), entity.Email),
|
||||
new(nameof(UserLicenseConstants.Premium), entity.Premium.ToString()),
|
||||
new(nameof(UserLicenseConstants.MaxStorageGb), entity.MaxStorageGb.ToString()),
|
||||
new(nameof(UserLicenseConstants.Issued), DateTime.UtcNow.ToString(CultureInfo.InvariantCulture)),
|
||||
new(nameof(UserLicenseConstants.Expires), expires.ToString()),
|
||||
new(nameof(UserLicenseConstants.Refresh), refresh.ToString()),
|
||||
new(nameof(UserLicenseConstants.Trial), trial.ToString()),
|
||||
};
|
||||
|
||||
return Task.FromResult(claims);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user