From 02fd8b0f3e63f19743d70bb1ac5788cadb66a4d6 Mon Sep 17 00:00:00 2001 From: Conner Turnbull Date: Mon, 28 Oct 2024 16:02:19 -0400 Subject: [PATCH 01/12] Added the ability to create a JWT on an organization license that contains all license properties as claims --- .../Extensions/ServiceCollectionExtensions.cs | 2 + .../Licenses/Extensions/LicenseExtensions.cs | 78 ++++++++++++++++++ .../LicenseServiceCollectionExtensions.cs | 14 ++++ .../Billing/Licenses/Models/LicenseContext.cs | 10 +++ .../Services/ILicenseClaimsFactory.cs | 9 +++ .../OrganizationLicenseClaimsFactory.cs | 79 +++++++++++++++++++ src/Core/Constants.cs | 1 + .../Models/Business/OrganizationLicense.cs | 2 + .../Cloud/CloudGetOrganizationLicenseQuery.cs | 6 +- src/Core/Services/ILicensingService.cs | 4 + .../Implementations/LicensingService.cs | 59 +++++++++++++- .../NoopLicensingService.cs | 5 ++ .../Utilities/ServiceCollectionExtensions.cs | 2 +- .../UpdateOrganizationLicenseCommandTests.cs | 2 +- 14 files changed, 268 insertions(+), 5 deletions(-) create mode 100644 src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs create mode 100644 src/Core/Billing/Licenses/Extensions/LicenseServiceCollectionExtensions.cs create mode 100644 src/Core/Billing/Licenses/Models/LicenseContext.cs create mode 100644 src/Core/Billing/Licenses/Services/ILicenseClaimsFactory.cs create mode 100644 src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs diff --git a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs index abfceac7366e..78253f739922 100644 --- a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs +++ b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs @@ -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 void AddBillingOperations(this IServiceCollection services) services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddLicenseServices(); } } diff --git a/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs b/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs new file mode 100644 index 000000000000..c6e7faf6ff42 --- /dev/null +++ b/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs @@ -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; + } +} diff --git a/src/Core/Billing/Licenses/Extensions/LicenseServiceCollectionExtensions.cs b/src/Core/Billing/Licenses/Extensions/LicenseServiceCollectionExtensions.cs new file mode 100644 index 000000000000..c67badae5020 --- /dev/null +++ b/src/Core/Billing/Licenses/Extensions/LicenseServiceCollectionExtensions.cs @@ -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, OrganizationLicenseClaimsFactory>(); + } +} diff --git a/src/Core/Billing/Licenses/Models/LicenseContext.cs b/src/Core/Billing/Licenses/Models/LicenseContext.cs new file mode 100644 index 000000000000..8dcc24e93917 --- /dev/null +++ b/src/Core/Billing/Licenses/Models/LicenseContext.cs @@ -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; } +} diff --git a/src/Core/Billing/Licenses/Services/ILicenseClaimsFactory.cs b/src/Core/Billing/Licenses/Services/ILicenseClaimsFactory.cs new file mode 100644 index 000000000000..926ad04683c2 --- /dev/null +++ b/src/Core/Billing/Licenses/Services/ILicenseClaimsFactory.cs @@ -0,0 +1,9 @@ +using System.Security.Claims; +using Bit.Core.Billing.Licenses.Models; + +namespace Bit.Core.Billing.Licenses.Services; + +public interface ILicenseClaimsFactory +{ + Task> GenerateClaims(T entity, LicenseContext licenseContext); +} diff --git a/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs b/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs new file mode 100644 index 000000000000..8f6a87c2c2b2 --- /dev/null +++ b/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs @@ -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 +{ + public Task> 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 + { + 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; +} diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 0bc6393d3cce..dfa4f93799ef 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -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 GetAllKeys() { diff --git a/src/Core/Models/Business/OrganizationLicense.cs b/src/Core/Models/Business/OrganizationLicense.cs index ea512736458c..f7b959068c47 100644 --- a/src/Core/Models/Business/OrganizationLicense.cs +++ b/src/Core/Models/Business/OrganizationLicense.cs @@ -151,6 +151,7 @@ public OrganizationLicense(Organization org, SubscriptionInfo subscriptionInfo, 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); /// @@ -176,6 +177,7 @@ public byte[] GetDataBytes(bool forHash = false) !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 diff --git a/src/Core/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs b/src/Core/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs index b8fad451e23e..a4b08736c2bd 100644 --- a/src/Core/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs +++ b/src/Core/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs @@ -33,6 +33,10 @@ public async Task GetLicenseAsync(Organization organization } 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) + }; } } diff --git a/src/Core/Services/ILicensingService.cs b/src/Core/Services/ILicensingService.cs index e92fa87fd6f5..ed478c467b1e 100644 --- a/src/Core/Services/ILicensingService.cs +++ b/src/Core/Services/ILicensingService.cs @@ -14,4 +14,8 @@ public interface ILicensingService Task ReadOrganizationLicenseAsync(Organization organization); Task ReadOrganizationLicenseAsync(Guid organizationId); + Task CreateOrganizationTokenAsync( + Organization organization, + Guid installationId, + SubscriptionInfo subscriptionInfo); } diff --git a/src/Core/Services/Implementations/LicensingService.cs b/src/Core/Services/Implementations/LicensingService.cs index 85b8f312001b..4128d72db51b 100644 --- a/src/Core/Services/Implementations/LicensingService.cs +++ b/src/Core/Services/Implementations/LicensingService.cs @@ -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 _logger; + private readonly ILicenseClaimsFactory _organizationLicenseClaimsFactory; + private readonly IFeatureService _featureService; private IDictionary _userCheckCache = new Dictionary(); @@ -32,7 +41,9 @@ public LicensingService( IMailService mailService, IWebHostEnvironment environment, ILogger logger, - IGlobalSettings globalSettings) + IGlobalSettings globalSettings, + ILicenseClaimsFactory organizationLicenseClaimsFactory, + IFeatureService featureService) { _userRepository = userRepository; _organizationRepository = organizationRepository; @@ -40,6 +51,8 @@ public LicensingService( _mailService = mailService; _logger = logger; _globalSettings = globalSettings; + _organizationLicenseClaimsFactory = organizationLicenseClaimsFactory; + _featureService = featureService; var certThumbprint = environment.IsDevelopment() ? "207E64A231E8AA32AAF68A61037C075EBEBD553F" : @@ -272,4 +285,46 @@ public async Task ReadOrganizationLicenseAsync(Guid organiz using var fs = File.OpenRead(filePath); return await JsonSerializer.DeserializeAsync(fs); } + + public async Task 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 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); + } } diff --git a/src/Core/Services/NoopImplementations/NoopLicensingService.cs b/src/Core/Services/NoopImplementations/NoopLicensingService.cs index 8eb42a318c8c..16470e2d5c5d 100644 --- a/src/Core/Services/NoopImplementations/NoopLicensingService.cs +++ b/src/Core/Services/NoopImplementations/NoopLicensingService.cs @@ -53,4 +53,9 @@ public Task ReadOrganizationLicenseAsync(Guid organizationI { return Task.FromResult(null); } + + public Task CreateOrganizationTokenAsync(Organization organization, Guid installationId, SubscriptionInfo subscriptionInfo) + { + return Task.FromResult(null); + } } diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 5a5585952460..931d643dfd52 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -226,7 +226,7 @@ public static void AddDefaultServices(this IServiceCollection services, GlobalSe services.AddScoped(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddScoped(); services.AddSingleton(_ => { var options = new LookupClientOptions { Timeout = TimeSpan.FromSeconds(15), UseTcpOnly = true }; diff --git a/test/Core.Test/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs index 565f2f32c472..0a6c98a2a8d9 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs @@ -80,7 +80,7 @@ await sutProvider.GetDependency() .ReplaceAndUpdateCacheAsync(Arg.Is( 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)); } From a473487e1930d4c0fa05c944098a7e10979ce1bc Mon Sep 17 00:00:00 2001 From: Conner Turnbull Date: Thu, 31 Oct 2024 11:09:04 -0400 Subject: [PATCH 02/12] Added the ability to create a JWT on a user license that contains all license properties as claims --- .../LicenseServiceCollectionExtensions.cs | 2 + .../UserLicenseClaimsFactory.cs | 38 +++++++++++++++++++ src/Core/Models/Business/UserLicense.cs | 2 + src/Core/Services/ILicensingService.cs | 2 + .../Implementations/LicensingService.cs | 23 +++++++++-- .../Services/Implementations/UserService.cs | 13 +++++-- .../NoopLicensingService.cs | 5 +++ 7 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 src/Core/Billing/Licenses/Services/Implementations/UserLicenseClaimsFactory.cs diff --git a/src/Core/Billing/Licenses/Extensions/LicenseServiceCollectionExtensions.cs b/src/Core/Billing/Licenses/Extensions/LicenseServiceCollectionExtensions.cs index c67badae5020..b08adbd0047a 100644 --- a/src/Core/Billing/Licenses/Extensions/LicenseServiceCollectionExtensions.cs +++ b/src/Core/Billing/Licenses/Extensions/LicenseServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ 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; @@ -10,5 +11,6 @@ public static class LicenseServiceCollectionExtensions public static void AddLicenseServices(this IServiceCollection services) { services.AddTransient, OrganizationLicenseClaimsFactory>(); + services.AddTransient, UserLicenseClaimsFactory>(); } } diff --git a/src/Core/Billing/Licenses/Services/Implementations/UserLicenseClaimsFactory.cs b/src/Core/Billing/Licenses/Services/Implementations/UserLicenseClaimsFactory.cs new file mode 100644 index 000000000000..59e2bd57bcf1 --- /dev/null +++ b/src/Core/Billing/Licenses/Services/Implementations/UserLicenseClaimsFactory.cs @@ -0,0 +1,38 @@ +using System.Globalization; +using System.Security.Claims; +using Bit.Core.Billing.Licenses.Models; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Business; + +namespace Bit.Core.Billing.Licenses.Services.Implementations; + +public class UserLicenseClaimsFactory : ILicenseClaimsFactory +{ + public Task> 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 + { + new(nameof(UserLicense.LicenseType), LicenseType.User.ToString()), + new(nameof(UserLicense.LicenseKey), entity.LicenseKey), + new(nameof(UserLicense.Id), entity.Id.ToString()), + new(nameof(UserLicense.Name), entity.Name), + new(nameof(UserLicense.Email), entity.Email), + new(nameof(UserLicense.Premium), entity.Premium.ToString()), + new(nameof(UserLicense.MaxStorageGb), entity.MaxStorageGb.ToString()), + new(nameof(UserLicense.Issued), DateTime.UtcNow.ToString(CultureInfo.InvariantCulture)), + new(nameof(UserLicense.Expires), expires.ToString()), + new(nameof(UserLicense.Refresh), refresh.ToString()), + new(nameof(UserLicense.Trial), trial.ToString()), + }; + + return Task.FromResult(claims); + } +} diff --git a/src/Core/Models/Business/UserLicense.cs b/src/Core/Models/Business/UserLicense.cs index 0f1b191a1d58..3e663b0c030c 100644 --- a/src/Core/Models/Business/UserLicense.cs +++ b/src/Core/Models/Business/UserLicense.cs @@ -70,6 +70,7 @@ public UserLicense(User user, ILicensingService licenseService, int? version = n 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); @@ -84,6 +85,7 @@ public byte[] GetDataBytes(bool forHash = false) !p.Name.Equals(nameof(Signature)) && !p.Name.Equals(nameof(SignatureBytes)) && !p.Name.Equals(nameof(LicenseType)) && + !p.Name.Equals(nameof(Token)) && ( !forHash || ( diff --git a/src/Core/Services/ILicensingService.cs b/src/Core/Services/ILicensingService.cs index ed478c467b1e..7458947d0efa 100644 --- a/src/Core/Services/ILicensingService.cs +++ b/src/Core/Services/ILicensingService.cs @@ -18,4 +18,6 @@ Task CreateOrganizationTokenAsync( Organization organization, Guid installationId, SubscriptionInfo subscriptionInfo); + + Task CreateUserTokenAsync(User user, SubscriptionInfo subscriptionInfo); } diff --git a/src/Core/Services/Implementations/LicensingService.cs b/src/Core/Services/Implementations/LicensingService.cs index 4128d72db51b..9e0580904198 100644 --- a/src/Core/Services/Implementations/LicensingService.cs +++ b/src/Core/Services/Implementations/LicensingService.cs @@ -26,10 +26,10 @@ public class LicensingService : ILicensingService private readonly IGlobalSettings _globalSettings; private readonly IUserRepository _userRepository; private readonly IOrganizationRepository _organizationRepository; - private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IMailService _mailService; private readonly ILogger _logger; private readonly ILicenseClaimsFactory _organizationLicenseClaimsFactory; + private readonly ILicenseClaimsFactory _userLicenseClaimsFactory; private readonly IFeatureService _featureService; private IDictionary _userCheckCache = new Dictionary(); @@ -37,22 +37,22 @@ public class LicensingService : ILicensingService public LicensingService( IUserRepository userRepository, IOrganizationRepository organizationRepository, - IOrganizationUserRepository organizationUserRepository, IMailService mailService, IWebHostEnvironment environment, ILogger logger, IGlobalSettings globalSettings, ILicenseClaimsFactory organizationLicenseClaimsFactory, - IFeatureService featureService) + IFeatureService featureService, + ILicenseClaimsFactory userLicenseClaimsFactory) { _userRepository = userRepository; _organizationRepository = organizationRepository; - _organizationUserRepository = organizationUserRepository; _mailService = mailService; _logger = logger; _globalSettings = globalSettings; _organizationLicenseClaimsFactory = organizationLicenseClaimsFactory; _featureService = featureService; + _userLicenseClaimsFactory = userLicenseClaimsFactory; var certThumbprint = environment.IsDevelopment() ? "207E64A231E8AA32AAF68A61037C075EBEBD553F" : @@ -305,6 +305,21 @@ public async Task CreateOrganizationTokenAsync(Organization organization return GenerateToken(claims, audience, expires); } + public async Task CreateUserTokenAsync(User user, SubscriptionInfo subscriptionInfo) + { + if (!_featureService.IsEnabled(FeatureFlagKeys.SelfHostLicenseRefactor)) + { + return null; + } + + var licenseContext = new LicenseContext { SubscriptionInfo = subscriptionInfo }; + var claims = await _userLicenseClaimsFactory.GenerateClaims(user, licenseContext); + var audience = user.Id.ToString(); + var expires = user.PremiumExpirationDate ?? DateTime.UtcNow.AddDays(7); + + return GenerateToken(claims, audience, expires); + } + private string GenerateToken(List claims, string audience, DateTime expires) { if (claims.All(claim => claim.Type != JwtClaimTypes.JwtId)) diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 2199d0a7afe0..948a580ab0c1 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -1111,7 +1111,9 @@ public async Task UpdatePremiumExpirationAsync(Guid userId, DateTime? expiration } } - public async Task GenerateLicenseAsync(User user, SubscriptionInfo subscriptionInfo = null, + public async Task GenerateLicenseAsync( + User user, + SubscriptionInfo subscriptionInfo = null, int? version = null) { if (user == null) @@ -1124,8 +1126,13 @@ public async Task GenerateLicenseAsync(User user, SubscriptionInfo subscriptionInfo = await _paymentService.GetSubscriptionAsync(user); } - return subscriptionInfo == null ? new UserLicense(user, _licenseService) : - new UserLicense(user, subscriptionInfo, _licenseService); + var userLicense = subscriptionInfo == null + ? new UserLicense(user, _licenseService) + : new UserLicense(user, subscriptionInfo, _licenseService); + + userLicense.Token = await _licenseService.CreateUserTokenAsync(user, subscriptionInfo); + + return userLicense; } public override async Task CheckPasswordAsync(User user, string password) diff --git a/src/Core/Services/NoopImplementations/NoopLicensingService.cs b/src/Core/Services/NoopImplementations/NoopLicensingService.cs index 16470e2d5c5d..288840179649 100644 --- a/src/Core/Services/NoopImplementations/NoopLicensingService.cs +++ b/src/Core/Services/NoopImplementations/NoopLicensingService.cs @@ -58,4 +58,9 @@ public Task CreateOrganizationTokenAsync(Organization organization, Guid { return Task.FromResult(null); } + + public Task CreateUserTokenAsync(User user, SubscriptionInfo subscriptionInfo) + { + return Task.FromResult(null); + } } From 6fa8e5afebe6a644a5271853f74291b5ac5472fa Mon Sep 17 00:00:00 2001 From: Conner Turnbull Date: Mon, 4 Nov 2024 14:58:45 -0500 Subject: [PATCH 03/12] Added ability to consume JWT licenses --- .../Licenses/Extensions/LicenseExtensions.cs | 31 +- src/Core/Models/Business/ILicense.cs | 1 + .../Models/Business/OrganizationLicense.cs | 368 ++++++++++++++---- src/Core/Models/Business/UserLicense.cs | 90 ++++- .../Implementations/LicensingService.cs | 33 +- 5 files changed, 443 insertions(+), 80 deletions(-) diff --git a/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs b/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs index c6e7faf6ff42..592df4e9d3ae 100644 --- a/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs +++ b/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs @@ -1,4 +1,6 @@ -using Bit.Core.AdminConsole.Entities; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Models.Business; @@ -6,7 +8,6 @@ namespace Bit.Core.Billing.Licenses.Extensions; public static class LicenseExtensions { - public static DateTime CalculateFreshExpirationDate(this Organization org, SubscriptionInfo subscriptionInfo) { if (subscriptionInfo?.Subscription == null) @@ -75,4 +76,30 @@ public static DateTime CalculateFreshExpirationDateWithoutGracePeriod(this Organ return expirationDate; } + + public static ClaimsPrincipal ToClaimsPrincipal(this string token) + { + var jwtSecurityTokenHandler = new JwtSecurityTokenHandler(); + var jwtSecurityToken = jwtSecurityTokenHandler.ReadJwtToken(token); + + if (jwtSecurityToken is null) + { + throw new ArgumentException("Invalid token.", nameof(token)); + } + + var claimsIdentity = new ClaimsIdentity(jwtSecurityToken.Claims, "BitwardenLicense"); + return new ClaimsPrincipal(claimsIdentity); + } + + public static T GetValue(this ClaimsPrincipal principal, string claimType) + { + var claim = principal.FindFirst(claimType); + + if (claim is null) + { + return default; + } + + return (T)Convert.ChangeType(claim.Value, typeof(T)); + } } diff --git a/src/Core/Models/Business/ILicense.cs b/src/Core/Models/Business/ILicense.cs index ad389b0a12b8..b0e295bdd987 100644 --- a/src/Core/Models/Business/ILicense.cs +++ b/src/Core/Models/Business/ILicense.cs @@ -12,6 +12,7 @@ public interface ILicense bool Trial { get; set; } string Hash { get; set; } string Signature { get; set; } + string Token { get; set; } byte[] SignatureBytes { get; } byte[] GetDataBytes(bool forHash = false); byte[] ComputeHash(); diff --git a/src/Core/Models/Business/OrganizationLicense.cs b/src/Core/Models/Business/OrganizationLicense.cs index f7b959068c47..42b4f8febab7 100644 --- a/src/Core/Models/Business/OrganizationLicense.cs +++ b/src/Core/Models/Business/OrganizationLicense.cs @@ -5,6 +5,7 @@ using System.Text.Json.Serialization; using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Licenses.Extensions; using Bit.Core.Enums; using Bit.Core.Services; using Bit.Core.Settings; @@ -239,6 +240,58 @@ public byte[] ComputeHash() } public bool CanUse(IGlobalSettings globalSettings, ILicensingService licensingService, out string exception) + { + if (string.IsNullOrWhiteSpace(Token)) + { + return ObsoleteCanUse(globalSettings, licensingService, out exception); + } + + var errorMessages = new StringBuilder(); + + var enabled = Token.ToClaimsPrincipal().GetValue(nameof(Enabled)); + if (!enabled) + { + errorMessages.AppendLine("Your cloud-hosted organization is currently disabled."); + } + + var installationId = Token.ToClaimsPrincipal().GetValue(nameof(InstallationId)); + if (installationId != globalSettings.Installation.Id) + { + errorMessages.AppendLine("The installation ID does not match the current installation."); + } + + var selfHost = Token.ToClaimsPrincipal().GetValue(nameof(SelfHost)); + if (!selfHost) + { + errorMessages.AppendLine("The license does not allow for on-premise hosting of organizations."); + } + + var licenseType = Token.ToClaimsPrincipal().GetValue(nameof(LicenseType)); + if (licenseType != Enums.LicenseType.Organization) + { + errorMessages.AppendLine("Premium licenses cannot be applied to an organization. " + + "Upload this license from your personal account settings page."); + } + + if (errorMessages.Length > 0) + { + exception = $"Invalid license. {errorMessages.ToString().TrimEnd()}"; + return false; + } + + exception = ""; + return true; + } + + /// + /// Do not extend this method. It is only here for backwards compatibility with old licenses. + /// Instead, extend the CanUse method using the ClaimsPrincipal. + /// + /// + /// + /// + /// + private bool ObsoleteCanUse(IGlobalSettings globalSettings, ILicensingService licensingService, out string exception) { var errorMessages = new StringBuilder(); @@ -294,100 +347,281 @@ public bool CanUse(IGlobalSettings globalSettings, ILicensingService licensingSe } public bool VerifyData(Organization organization, IGlobalSettings globalSettings) + { + if (string.IsNullOrWhiteSpace(Token)) + { + return ObsoleteVerifyData(organization, globalSettings); + } + + // It looks a little goofy to extend this file instead of moving this logic to a command, but in an effort + // to keep changes low in each PR, we'll move this to a command in a subsequent PR. + // The end goal will be to not have an OrganizationLicense file whatsoever, and instead simply rely on the token + var claimsPrincipal = Token.ToClaimsPrincipal(); + + var issued = claimsPrincipal.GetValue(nameof(Issued)); + var expires = claimsPrincipal.GetValue(nameof(Expires)); + + if (issued > DateTime.UtcNow || expires < DateTime.UtcNow) + { + return false; + } + + var installationId = claimsPrincipal.GetValue(nameof(InstallationId)); + if (installationId != globalSettings.Installation.Id) + { + return false; + } + + var licenseKey = claimsPrincipal.GetValue(nameof(LicenseKey)); + if (licenseKey != organization.LicenseKey) + { + return false; + } + + var enabled = claimsPrincipal.GetValue(nameof(Enabled)); + if (enabled != organization.Enabled) + { + return false; + } + + var planType = claimsPrincipal.GetValue(nameof(PlanType)); + if (planType != organization.PlanType) + { + return false; + } + + var seats = claimsPrincipal.GetValue(nameof(Seats)); + if (seats != organization.Seats) + { + return false; + } + + var maxCollections = claimsPrincipal.GetValue(nameof(MaxCollections)); + if (maxCollections != organization.MaxCollections) + { + return false; + } + + var useGroups = claimsPrincipal.GetValue(nameof(UseGroups)); + if (useGroups != organization.UseGroups) + { + return false; + } + + var useDirectory = claimsPrincipal.GetValue(nameof(UseDirectory)); + if (useDirectory != organization.UseDirectory) + { + return false; + } + + var useTotp = claimsPrincipal.GetValue(nameof(UseTotp)); + if (useTotp != organization.UseTotp) + { + return false; + } + + var selfHost = claimsPrincipal.GetValue(nameof(SelfHost)); + if (selfHost != organization.SelfHost) + { + return false; + } + + var name = claimsPrincipal.GetValue(nameof(Name)); + if (name != organization.Name) + { + return false; + } + + var usersGetPremium = claimsPrincipal.GetValue(nameof(UsersGetPremium)); + if (usersGetPremium != organization.UsersGetPremium) + { + return false; + } + + var useEvents = claimsPrincipal.GetValue(nameof(UseEvents)); + if (useEvents != organization.UseEvents) + { + return false; + } + + var use2fa = claimsPrincipal.GetValue(nameof(Use2fa)); + if (use2fa != organization.Use2fa) + { + return false; + } + + var useApi = claimsPrincipal.GetValue(nameof(UseApi)); + if (useApi != organization.UseApi) + { + return false; + } + + var usePolicies = claimsPrincipal.GetValue(nameof(UsePolicies)); + if (usePolicies != organization.UsePolicies) + { + return false; + } + + var useSso = claimsPrincipal.GetValue(nameof(UseSso)); + if (useSso != organization.UseSso) + { + return false; + } + + var useResetPassword = claimsPrincipal.GetValue(nameof(UseResetPassword)); + if (useResetPassword != organization.UseResetPassword) + { + return false; + } + + var useKeyConnector = claimsPrincipal.GetValue(nameof(UseKeyConnector)); + if (useKeyConnector != organization.UseKeyConnector) + { + return false; + } + + var useScim = claimsPrincipal.GetValue(nameof(UseScim)); + if (useScim != organization.UseScim) + { + return false; + } + + var useCustomPermissions = claimsPrincipal.GetValue(nameof(UseCustomPermissions)); + if (useCustomPermissions != organization.UseCustomPermissions) + { + return false; + } + + var useSecretsManager = claimsPrincipal.GetValue(nameof(UseSecretsManager)); + if (useSecretsManager != organization.UseSecretsManager) + { + return false; + } + + var usePasswordManager = claimsPrincipal.GetValue(nameof(UsePasswordManager)); + if (usePasswordManager != organization.UsePasswordManager) + { + return false; + } + + var smSeats = claimsPrincipal.GetValue(nameof(SmSeats)); + if (smSeats != organization.SmSeats) + { + return false; + } + + var smServiceAccounts = claimsPrincipal.GetValue(nameof(SmServiceAccounts)); + if (smServiceAccounts != organization.SmServiceAccounts) + { + return false; + } + + return true; + } + + /// + /// Do not extend this method. It is only here for backwards compatibility with old licenses. + /// Instead, extend the CanUse method using the ClaimsPrincipal. + /// + /// + /// + /// + /// + private bool ObsoleteVerifyData(Organization organization, IGlobalSettings globalSettings) { if (Issued > DateTime.UtcNow || Expires < DateTime.UtcNow) { return false; } - if (ValidLicenseVersion) + if (!ValidLicenseVersion) { - var valid = - globalSettings.Installation.Id == InstallationId && - organization.LicenseKey != null && organization.LicenseKey.Equals(LicenseKey) && - organization.Enabled == Enabled && - organization.PlanType == PlanType && - organization.Seats == Seats && - organization.MaxCollections == MaxCollections && - organization.UseGroups == UseGroups && - organization.UseDirectory == UseDirectory && - organization.UseTotp == UseTotp && - organization.SelfHost == SelfHost && - organization.Name.Equals(Name); - - if (valid && Version >= 2) - { - valid = organization.UsersGetPremium == UsersGetPremium; - } + throw new NotSupportedException($"Version {Version} is not supported."); + } - if (valid && Version >= 3) - { - valid = organization.UseEvents == UseEvents; - } + var valid = + globalSettings.Installation.Id == InstallationId && + organization.LicenseKey != null && organization.LicenseKey.Equals(LicenseKey) && + organization.Enabled == Enabled && + organization.PlanType == PlanType && + organization.Seats == Seats && + organization.MaxCollections == MaxCollections && + organization.UseGroups == UseGroups && + organization.UseDirectory == UseDirectory && + organization.UseTotp == UseTotp && + organization.SelfHost == SelfHost && + organization.Name.Equals(Name); + + if (valid && Version >= 2) + { + valid = organization.UsersGetPremium == UsersGetPremium; + } - if (valid && Version >= 4) - { - valid = organization.Use2fa == Use2fa; - } + if (valid && Version >= 3) + { + valid = organization.UseEvents == UseEvents; + } - if (valid && Version >= 5) - { - valid = organization.UseApi == UseApi; - } + if (valid && Version >= 4) + { + valid = organization.Use2fa == Use2fa; + } - if (valid && Version >= 6) - { - valid = organization.UsePolicies == UsePolicies; - } + if (valid && Version >= 5) + { + valid = organization.UseApi == UseApi; + } - if (valid && Version >= 7) - { - valid = organization.UseSso == UseSso; - } + if (valid && Version >= 6) + { + valid = organization.UsePolicies == UsePolicies; + } - if (valid && Version >= 8) - { - valid = organization.UseResetPassword == UseResetPassword; - } + if (valid && Version >= 7) + { + valid = organization.UseSso == UseSso; + } - if (valid && Version >= 9) - { - valid = organization.UseKeyConnector == UseKeyConnector; - } + if (valid && Version >= 8) + { + valid = organization.UseResetPassword == UseResetPassword; + } - if (valid && Version >= 10) - { - valid = organization.UseScim == UseScim; - } + if (valid && Version >= 9) + { + valid = organization.UseKeyConnector == UseKeyConnector; + } - if (valid && Version >= 11) - { - valid = organization.UseCustomPermissions == UseCustomPermissions; - } + if (valid && Version >= 10) + { + valid = organization.UseScim == UseScim; + } + + if (valid && Version >= 11) + { + valid = organization.UseCustomPermissions == UseCustomPermissions; + } - /*Version 12 added ExpirationWithoutDatePeriod, but that property is informational only and is not saved + /*Version 12 added ExpirationWithoutDatePeriod, but that property is informational only and is not saved to the Organization object. It's validated as part of the hash but does not need to be validated here. */ - if (valid && Version >= 13) - { - valid = organization.UseSecretsManager == UseSecretsManager && - organization.UsePasswordManager == UsePasswordManager && - organization.SmSeats == SmSeats && - organization.SmServiceAccounts == SmServiceAccounts; - } + if (valid && Version >= 13) + { + valid = organization.UseSecretsManager == UseSecretsManager && + organization.UsePasswordManager == UsePasswordManager && + organization.SmSeats == SmSeats && + organization.SmServiceAccounts == SmServiceAccounts; + } - /* + /* * Version 14 added LimitCollectionCreationDeletion and Version * 15 added AllowAdminAccessToAllCollectionItems, however they * are no longer used and are intentionally excluded from * validation. */ - return valid; - } - - throw new NotSupportedException($"Version {Version} is not supported."); + return valid; } public bool VerifySignature(X509Certificate2 certificate) diff --git a/src/Core/Models/Business/UserLicense.cs b/src/Core/Models/Business/UserLicense.cs index 3e663b0c030c..c5b655b5f8bc 100644 --- a/src/Core/Models/Business/UserLicense.cs +++ b/src/Core/Models/Business/UserLicense.cs @@ -3,6 +3,7 @@ using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.Json.Serialization; +using Bit.Core.Billing.Licenses.Extensions; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Services; @@ -116,6 +117,46 @@ public byte[] ComputeHash() } public bool CanUse(User user, out string exception) + { + if (string.IsNullOrWhiteSpace(Token)) + { + return ObsoleteCanUse(user, out exception); + } + + var errorMessages = new StringBuilder(); + var claimsPrincipal = Token.ToClaimsPrincipal(); + + var emailVerified = claimsPrincipal.GetValue(nameof(User.EmailVerified)); + if (!emailVerified) + { + errorMessages.AppendLine("The user's email is not verified."); + } + + var email = claimsPrincipal.GetValue(nameof(Email)); + if (!email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase)) + { + errorMessages.AppendLine("The user's email does not match the license email."); + } + + if (errorMessages.Length > 0) + { + exception = $"Invalid license. {errorMessages.ToString().TrimEnd()}"; + return false; + } + + exception = ""; + return true; + } + + /// + /// Do not extend this method. It is only here for backwards compatibility with old licenses. + /// Instead, extend the CanUse method using the ClaimsPrincipal. + /// + /// + /// + /// + /// + private bool ObsoleteCanUse(User user, out string exception) { var errorMessages = new StringBuilder(); @@ -155,21 +196,58 @@ public bool CanUse(User user, out string exception) } public bool VerifyData(User user) + { + if (string.IsNullOrWhiteSpace(Token)) + { + return ObsoleteVerifyData(user); + } + + var claimsPrincipal = Token.ToClaimsPrincipal(); + + var licenseKey = claimsPrincipal.GetValue(nameof(LicenseKey)); + if (licenseKey != user.LicenseKey) + { + return false; + } + + var premium = claimsPrincipal.GetValue(nameof(Premium)); + if (premium != user.Premium) + { + return false; + } + + var email = claimsPrincipal.GetValue(nameof(Email)); + if (!email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase)) + { + return false; + } + + return true; + } + + /// + /// Do not extend this method. It is only here for backwards compatibility with old licenses. + /// Instead, extend the CanUse method using the ClaimsPrincipal. + /// + /// + /// + /// + private bool ObsoleteVerifyData(User user) { if (Issued > DateTime.UtcNow || Expires < DateTime.UtcNow) { return false; } - if (Version == 1) + if (Version != 1) { - return - user.LicenseKey != null && user.LicenseKey.Equals(LicenseKey) && - user.Premium == Premium && - user.Email.Equals(Email, StringComparison.InvariantCultureIgnoreCase); + throw new NotSupportedException($"Version {Version} is not supported."); } - throw new NotSupportedException($"Version {Version} is not supported."); + return + user.LicenseKey != null && user.LicenseKey.Equals(LicenseKey) && + user.Premium == Premium && + user.Email.Equals(Email, StringComparison.InvariantCultureIgnoreCase); } public bool VerifySignature(X509Certificate2 certificate) diff --git a/src/Core/Services/Implementations/LicensingService.cs b/src/Core/Services/Implementations/LicensingService.cs index 9e0580904198..d088ea66a558 100644 --- a/src/Core/Services/Implementations/LicensingService.cs +++ b/src/Core/Services/Implementations/LicensingService.cs @@ -117,13 +117,19 @@ public async Task ValidateOrganizationsAsync() continue; } + if (!string.IsNullOrWhiteSpace(license.Token) && !VerifyToken(license.Token)) + { + await DisableOrganizationAsync(org, license, "Invalid token."); + continue; + } + if (!license.VerifyData(org, _globalSettings)) { await DisableOrganizationAsync(org, license, "Invalid data."); continue; } - if (!license.VerifySignature(_certificate)) + if (string.IsNullOrWhiteSpace(license.Token) && !license.VerifySignature(_certificate)) { await DisableOrganizationAsync(org, license, "Invalid signature."); continue; @@ -222,7 +228,7 @@ private async Task ProcessUserValidationAsync(User user) return false; } - if (!license.VerifySignature(_certificate)) + if (string.IsNullOrWhiteSpace(license.Token) && !license.VerifySignature(_certificate)) { await DisablePremiumAsync(user, license, "Invalid signature."); return false; @@ -247,7 +253,9 @@ private async Task DisablePremiumAsync(User user, ILicense license, string reaso public bool VerifyLicense(ILicense license) { - return license.VerifySignature(_certificate); + return string.IsNullOrWhiteSpace(license.Token) + ? license.VerifySignature(_certificate) + : VerifyToken(license.Token); } public byte[] SignLicense(ILicense license) @@ -300,8 +308,9 @@ public async Task CreateOrganizationTokenAsync(Organization organization }; var claims = await _organizationLicenseClaimsFactory.GenerateClaims(organization, licenseContext); - var audience = organization.Id.ToString(); + var audience = $"organization:{organization.Id}"; var expires = organization.CalculateFreshExpirationDate(subscriptionInfo); + return GenerateToken(claims, audience, expires); } @@ -314,7 +323,7 @@ public async Task CreateUserTokenAsync(User user, SubscriptionInfo subsc var licenseContext = new LicenseContext { SubscriptionInfo = subscriptionInfo }; var claims = await _userLicenseClaimsFactory.GenerateClaims(user, licenseContext); - var audience = user.Id.ToString(); + var audience = $"user:{user.Id}"; var expires = user.PremiumExpirationDate ?? DateTime.UtcNow.AddDays(7); return GenerateToken(claims, audience, expires); @@ -342,4 +351,18 @@ private string GenerateToken(List claims, string audience, DateTime expir var token = tokenHandler.CreateToken(tokenDescriptor); return tokenHandler.WriteToken(token); } + + private bool VerifyToken(string token) + { + try + { + _ = token.ToClaimsPrincipal(); + return true; + } + catch (Exception e) + { + _logger.LogWarning(e, "Invalid token."); + return false; + } + } } From e5357d9aa376132c965502ce628f4f3631aee687 Mon Sep 17 00:00:00 2001 From: Conner Turnbull Date: Mon, 4 Nov 2024 15:40:21 -0500 Subject: [PATCH 04/12] Resolved generic type issues when getting claim value --- .../Licenses/Extensions/LicenseExtensions.cs | 63 ++++++++++++++++++- .../Models/Business/OrganizationLicense.cs | 10 +-- 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs b/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs index 592df4e9d3ae..b517a6ad8332 100644 --- a/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs +++ b/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs @@ -100,6 +100,67 @@ public static T GetValue(this ClaimsPrincipal principal, string claimType) return default; } - return (T)Convert.ChangeType(claim.Value, typeof(T)); + // 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); } } diff --git a/src/Core/Models/Business/OrganizationLicense.cs b/src/Core/Models/Business/OrganizationLicense.cs index 42b4f8febab7..36a275fbc996 100644 --- a/src/Core/Models/Business/OrganizationLicense.cs +++ b/src/Core/Models/Business/OrganizationLicense.cs @@ -248,25 +248,27 @@ public bool CanUse(IGlobalSettings globalSettings, ILicensingService licensingSe var errorMessages = new StringBuilder(); - var enabled = Token.ToClaimsPrincipal().GetValue(nameof(Enabled)); + var claimsPrincipal = Token.ToClaimsPrincipal(); + + var enabled = claimsPrincipal.GetValue(nameof(Enabled)); if (!enabled) { errorMessages.AppendLine("Your cloud-hosted organization is currently disabled."); } - var installationId = Token.ToClaimsPrincipal().GetValue(nameof(InstallationId)); + var installationId = claimsPrincipal.GetValue(nameof(InstallationId)); if (installationId != globalSettings.Installation.Id) { errorMessages.AppendLine("The installation ID does not match the current installation."); } - var selfHost = Token.ToClaimsPrincipal().GetValue(nameof(SelfHost)); + var selfHost = claimsPrincipal.GetValue(nameof(SelfHost)); if (!selfHost) { errorMessages.AppendLine("The license does not allow for on-premise hosting of organizations."); } - var licenseType = Token.ToClaimsPrincipal().GetValue(nameof(LicenseType)); + var licenseType = claimsPrincipal.GetValue(nameof(LicenseType)); if (licenseType != Enums.LicenseType.Organization) { errorMessages.AppendLine("Premium licenses cannot be applied to an organization. " + From f650a44486c2a4cdf6edc4cc93154cddfabd69f5 Mon Sep 17 00:00:00 2001 From: Conner Turnbull Date: Thu, 7 Nov 2024 14:42:53 -0500 Subject: [PATCH 05/12] Now validating the jwt signature, exp, and iat --- .../Licenses/Extensions/LicenseExtensions.cs | 16 +- src/Core/Models/Business/ILicense.cs | 4 +- .../Models/Business/OrganizationLicense.cs | 239 +++++------------- src/Core/Models/Business/UserLicense.cs | 42 ++- src/Core/Services/ILicensingService.cs | 4 +- .../Implementations/LicensingService.cs | 50 +++- .../NoopLicensingService.cs | 8 +- .../UpdateOrganizationLicenseCommandTests.cs | 9 +- 8 files changed, 145 insertions(+), 227 deletions(-) diff --git a/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs b/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs index b517a6ad8332..92bfccdeda6c 100644 --- a/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs +++ b/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs @@ -1,8 +1,10 @@ using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; +using System.Security.Cryptography.X509Certificates; using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Models.Business; +using Microsoft.IdentityModel.Tokens; namespace Bit.Core.Billing.Licenses.Extensions; @@ -77,20 +79,6 @@ public static DateTime CalculateFreshExpirationDateWithoutGracePeriod(this Organ return expirationDate; } - public static ClaimsPrincipal ToClaimsPrincipal(this string token) - { - var jwtSecurityTokenHandler = new JwtSecurityTokenHandler(); - var jwtSecurityToken = jwtSecurityTokenHandler.ReadJwtToken(token); - - if (jwtSecurityToken is null) - { - throw new ArgumentException("Invalid token.", nameof(token)); - } - - var claimsIdentity = new ClaimsIdentity(jwtSecurityToken.Claims, "BitwardenLicense"); - return new ClaimsPrincipal(claimsIdentity); - } - public static T GetValue(this ClaimsPrincipal principal, string claimType) { var claim = principal.FindFirst(claimType); diff --git a/src/Core/Models/Business/ILicense.cs b/src/Core/Models/Business/ILicense.cs index b0e295bdd987..b1c2720b0534 100644 --- a/src/Core/Models/Business/ILicense.cs +++ b/src/Core/Models/Business/ILicense.cs @@ -1,4 +1,5 @@ -using System.Security.Cryptography.X509Certificates; +using System.Security.Claims; +using System.Security.Cryptography.X509Certificates; namespace Bit.Core.Models.Business; @@ -14,6 +15,7 @@ public interface ILicense string Signature { get; set; } string Token { get; set; } byte[] SignatureBytes { get; } + ClaimsPrincipal ClaimsPrincipal { get; set; } byte[] GetDataBytes(bool forHash = false); byte[] ComputeHash(); bool VerifySignature(X509Certificate2 certificate); diff --git a/src/Core/Models/Business/OrganizationLicense.cs b/src/Core/Models/Business/OrganizationLicense.cs index 36a275fbc996..8d437ae0f5d3 100644 --- a/src/Core/Models/Business/OrganizationLicense.cs +++ b/src/Core/Models/Business/OrganizationLicense.cs @@ -1,4 +1,5 @@ using System.Reflection; +using System.Security.Claims; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; @@ -105,6 +106,9 @@ public OrganizationLicense(Organization org, SubscriptionInfo subscriptionInfo, Hash = Convert.ToBase64String(ComputeHash()); Signature = Convert.ToBase64String(licenseService.SignLicense(this)); + ClaimsPrincipal = string.IsNullOrWhiteSpace(Token) + ? null + : licenseService.GetClaimsPrincipalFromToken(Token, $"organization:{Id}"); } public string LicenseKey { get; set; } @@ -154,6 +158,7 @@ public OrganizationLicense(Organization org, SubscriptionInfo subscriptionInfo, public string Signature { get; set; } public string Token { get; set; } [JsonIgnore] public byte[] SignatureBytes => Convert.FromBase64String(Signature); + [JsonIgnore] public ClaimsPrincipal ClaimsPrincipal { get; set; } /// /// Represents the current version of the license format. Should be updated whenever new fields are added. @@ -179,6 +184,7 @@ public byte[] GetDataBytes(bool forHash = false) !p.Name.Equals(nameof(SignatureBytes)) && !p.Name.Equals(nameof(LicenseType)) && !p.Name.Equals(nameof(Token)) && + !p.Name.Equals(nameof(ClaimsPrincipal)) && // UsersGetPremium was added in Version 2 (Version >= 2 || !p.Name.Equals(nameof(UsersGetPremium))) && // UseEvents was added in Version 3 @@ -248,27 +254,25 @@ public bool CanUse(IGlobalSettings globalSettings, ILicensingService licensingSe var errorMessages = new StringBuilder(); - var claimsPrincipal = Token.ToClaimsPrincipal(); - - var enabled = claimsPrincipal.GetValue(nameof(Enabled)); + var enabled = ClaimsPrincipal.GetValue(nameof(Enabled)); if (!enabled) { errorMessages.AppendLine("Your cloud-hosted organization is currently disabled."); } - var installationId = claimsPrincipal.GetValue(nameof(InstallationId)); + var installationId = ClaimsPrincipal.GetValue(nameof(InstallationId)); if (installationId != globalSettings.Installation.Id) { errorMessages.AppendLine("The installation ID does not match the current installation."); } - var selfHost = claimsPrincipal.GetValue(nameof(SelfHost)); + var selfHost = ClaimsPrincipal.GetValue(nameof(SelfHost)); if (!selfHost) { errorMessages.AppendLine("The license does not allow for on-premise hosting of organizations."); } - var licenseType = claimsPrincipal.GetValue(nameof(LicenseType)); + var licenseType = ClaimsPrincipal.GetValue(nameof(LicenseType)); if (licenseType != Enums.LicenseType.Organization) { errorMessages.AppendLine("Premium licenses cannot be applied to an organization. " + @@ -295,6 +299,7 @@ public bool CanUse(IGlobalSettings globalSettings, ILicensingService licensingSe /// private bool ObsoleteCanUse(IGlobalSettings globalSettings, ILicensingService licensingService, out string exception) { + // Do not extend this method. It is only here for backwards compatibility with old licenses. var errorMessages = new StringBuilder(); if (!Enabled) @@ -355,175 +360,66 @@ public bool VerifyData(Organization organization, IGlobalSettings globalSettings return ObsoleteVerifyData(organization, globalSettings); } - // It looks a little goofy to extend this file instead of moving this logic to a command, but in an effort - // to keep changes low in each PR, we'll move this to a command in a subsequent PR. - // The end goal will be to not have an OrganizationLicense file whatsoever, and instead simply rely on the token - var claimsPrincipal = Token.ToClaimsPrincipal(); - - var issued = claimsPrincipal.GetValue(nameof(Issued)); - var expires = claimsPrincipal.GetValue(nameof(Expires)); - - if (issued > DateTime.UtcNow || expires < DateTime.UtcNow) - { - return false; - } - - var installationId = claimsPrincipal.GetValue(nameof(InstallationId)); - if (installationId != globalSettings.Installation.Id) - { - return false; - } - - var licenseKey = claimsPrincipal.GetValue(nameof(LicenseKey)); - if (licenseKey != organization.LicenseKey) - { - return false; - } - - var enabled = claimsPrincipal.GetValue(nameof(Enabled)); - if (enabled != organization.Enabled) - { - return false; - } - - var planType = claimsPrincipal.GetValue(nameof(PlanType)); - if (planType != organization.PlanType) - { - return false; - } - - var seats = claimsPrincipal.GetValue(nameof(Seats)); - if (seats != organization.Seats) - { - return false; - } - - var maxCollections = claimsPrincipal.GetValue(nameof(MaxCollections)); - if (maxCollections != organization.MaxCollections) - { - return false; - } - - var useGroups = claimsPrincipal.GetValue(nameof(UseGroups)); - if (useGroups != organization.UseGroups) - { - return false; - } - - var useDirectory = claimsPrincipal.GetValue(nameof(UseDirectory)); - if (useDirectory != organization.UseDirectory) - { - return false; - } - - var useTotp = claimsPrincipal.GetValue(nameof(UseTotp)); - if (useTotp != organization.UseTotp) - { - return false; - } - - var selfHost = claimsPrincipal.GetValue(nameof(SelfHost)); - if (selfHost != organization.SelfHost) - { - return false; - } - - var name = claimsPrincipal.GetValue(nameof(Name)); - if (name != organization.Name) - { - return false; - } - - var usersGetPremium = claimsPrincipal.GetValue(nameof(UsersGetPremium)); - if (usersGetPremium != organization.UsersGetPremium) - { - return false; - } - - var useEvents = claimsPrincipal.GetValue(nameof(UseEvents)); - if (useEvents != organization.UseEvents) - { - return false; - } - - var use2fa = claimsPrincipal.GetValue(nameof(Use2fa)); - if (use2fa != organization.Use2fa) - { - return false; - } - - var useApi = claimsPrincipal.GetValue(nameof(UseApi)); - if (useApi != organization.UseApi) - { - return false; - } - - var usePolicies = claimsPrincipal.GetValue(nameof(UsePolicies)); - if (usePolicies != organization.UsePolicies) - { - return false; - } - - var useSso = claimsPrincipal.GetValue(nameof(UseSso)); - if (useSso != organization.UseSso) - { - return false; - } - - var useResetPassword = claimsPrincipal.GetValue(nameof(UseResetPassword)); - if (useResetPassword != organization.UseResetPassword) - { - return false; - } - - var useKeyConnector = claimsPrincipal.GetValue(nameof(UseKeyConnector)); - if (useKeyConnector != organization.UseKeyConnector) - { - return false; - } - - var useScim = claimsPrincipal.GetValue(nameof(UseScim)); - if (useScim != organization.UseScim) - { - return false; - } - - var useCustomPermissions = claimsPrincipal.GetValue(nameof(UseCustomPermissions)); - if (useCustomPermissions != organization.UseCustomPermissions) - { - return false; - } - - var useSecretsManager = claimsPrincipal.GetValue(nameof(UseSecretsManager)); - if (useSecretsManager != organization.UseSecretsManager) - { - return false; - } - - var usePasswordManager = claimsPrincipal.GetValue(nameof(UsePasswordManager)); - if (usePasswordManager != organization.UsePasswordManager) - { - return false; - } - - var smSeats = claimsPrincipal.GetValue(nameof(SmSeats)); - if (smSeats != organization.SmSeats) - { - return false; - } - - var smServiceAccounts = claimsPrincipal.GetValue(nameof(SmServiceAccounts)); - if (smServiceAccounts != organization.SmServiceAccounts) - { - return false; - } - - return true; + var issued = ClaimsPrincipal.GetValue(nameof(Issued)); + var expires = ClaimsPrincipal.GetValue(nameof(Expires)); + var installationId = ClaimsPrincipal.GetValue(nameof(InstallationId)); + var licenseKey = ClaimsPrincipal.GetValue(nameof(LicenseKey)); + var enabled = ClaimsPrincipal.GetValue(nameof(Enabled)); + var planType = ClaimsPrincipal.GetValue(nameof(PlanType)); + var seats = ClaimsPrincipal.GetValue(nameof(Seats)); + var maxCollections = ClaimsPrincipal.GetValue(nameof(MaxCollections)); + var useGroups = ClaimsPrincipal.GetValue(nameof(UseGroups)); + var useDirectory = ClaimsPrincipal.GetValue(nameof(UseDirectory)); + var useTotp = ClaimsPrincipal.GetValue(nameof(UseTotp)); + var selfHost = ClaimsPrincipal.GetValue(nameof(SelfHost)); + var name = ClaimsPrincipal.GetValue(nameof(Name)); + var usersGetPremium = ClaimsPrincipal.GetValue(nameof(UsersGetPremium)); + var useEvents = ClaimsPrincipal.GetValue(nameof(UseEvents)); + var use2fa = ClaimsPrincipal.GetValue(nameof(Use2fa)); + var useApi = ClaimsPrincipal.GetValue(nameof(UseApi)); + var usePolicies = ClaimsPrincipal.GetValue(nameof(UsePolicies)); + var useSso = ClaimsPrincipal.GetValue(nameof(UseSso)); + var useResetPassword = ClaimsPrincipal.GetValue(nameof(UseResetPassword)); + var useKeyConnector = ClaimsPrincipal.GetValue(nameof(UseKeyConnector)); + var useScim = ClaimsPrincipal.GetValue(nameof(UseScim)); + var useCustomPermissions = ClaimsPrincipal.GetValue(nameof(UseCustomPermissions)); + var useSecretsManager = ClaimsPrincipal.GetValue(nameof(UseSecretsManager)); + var usePasswordManager = ClaimsPrincipal.GetValue(nameof(UsePasswordManager)); + var smSeats = ClaimsPrincipal.GetValue(nameof(SmSeats)); + var smServiceAccounts = ClaimsPrincipal.GetValue(nameof(SmServiceAccounts)); + + return issued <= DateTime.UtcNow && + expires >= DateTime.UtcNow && + installationId == globalSettings.Installation.Id && + licenseKey == organization.LicenseKey && + enabled == organization.Enabled && + planType == organization.PlanType && + seats == organization.Seats && + maxCollections == organization.MaxCollections && + useGroups == organization.UseGroups && + useDirectory == organization.UseDirectory && + useTotp == organization.UseTotp && + selfHost == organization.SelfHost && + name == organization.Name && + usersGetPremium == organization.UsersGetPremium && + useEvents == organization.UseEvents && + use2fa == organization.Use2fa && + useApi == organization.UseApi && + usePolicies == organization.UsePolicies && + useSso == organization.UseSso && + useResetPassword == organization.UseResetPassword && + useKeyConnector == organization.UseKeyConnector && + useScim == organization.UseScim && + useCustomPermissions == organization.UseCustomPermissions && + useSecretsManager == organization.UseSecretsManager && + usePasswordManager == organization.UsePasswordManager && + smSeats == organization.SmSeats && + smServiceAccounts == organization.SmServiceAccounts; } /// /// Do not extend this method. It is only here for backwards compatibility with old licenses. - /// Instead, extend the CanUse method using the ClaimsPrincipal. + /// Instead, extend the VerifyData method using the ClaimsPrincipal. /// /// /// @@ -531,6 +427,7 @@ public bool VerifyData(Organization organization, IGlobalSettings globalSettings /// private bool ObsoleteVerifyData(Organization organization, IGlobalSettings globalSettings) { + // Do not extend this method. It is only here for backwards compatibility with old licenses. if (Issued > DateTime.UtcNow || Expires < DateTime.UtcNow) { return false; diff --git a/src/Core/Models/Business/UserLicense.cs b/src/Core/Models/Business/UserLicense.cs index c5b655b5f8bc..236e5e6a4793 100644 --- a/src/Core/Models/Business/UserLicense.cs +++ b/src/Core/Models/Business/UserLicense.cs @@ -1,4 +1,5 @@ using System.Reflection; +using System.Security.Claims; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; @@ -55,6 +56,9 @@ public UserLicense(User user, ILicensingService licenseService, int? version = n Hash = Convert.ToBase64String(ComputeHash()); Signature = Convert.ToBase64String(licenseService.SignLicense(this)); + ClaimsPrincipal = string.IsNullOrWhiteSpace(Token) + ? null + : licenseService.GetClaimsPrincipalFromToken(Token, $"user:{Id}"); } public string LicenseKey { get; set; } @@ -74,6 +78,8 @@ public UserLicense(User user, ILicensingService licenseService, int? version = n public string Token { get; set; } [JsonIgnore] public byte[] SignatureBytes => Convert.FromBase64String(Signature); + [JsonIgnore] + public ClaimsPrincipal ClaimsPrincipal { get; set; } public byte[] GetDataBytes(bool forHash = false) { @@ -87,6 +93,7 @@ public byte[] GetDataBytes(bool forHash = false) !p.Name.Equals(nameof(SignatureBytes)) && !p.Name.Equals(nameof(LicenseType)) && !p.Name.Equals(nameof(Token)) && + !p.Name.Equals(nameof(ClaimsPrincipal)) && ( !forHash || ( @@ -124,15 +131,14 @@ public bool CanUse(User user, out string exception) } var errorMessages = new StringBuilder(); - var claimsPrincipal = Token.ToClaimsPrincipal(); - var emailVerified = claimsPrincipal.GetValue(nameof(User.EmailVerified)); + var emailVerified = ClaimsPrincipal.GetValue(nameof(User.EmailVerified)); if (!emailVerified) { errorMessages.AppendLine("The user's email is not verified."); } - var email = claimsPrincipal.GetValue(nameof(Email)); + var email = ClaimsPrincipal.GetValue(nameof(Email)); if (!email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase)) { errorMessages.AppendLine("The user's email does not match the license email."); @@ -158,6 +164,7 @@ public bool CanUse(User user, out string exception) /// private bool ObsoleteCanUse(User user, out string exception) { + // Do not extend this method. It is only here for backwards compatibility with old licenses. var errorMessages = new StringBuilder(); if (Issued > DateTime.UtcNow) @@ -202,38 +209,25 @@ public bool VerifyData(User user) return ObsoleteVerifyData(user); } - var claimsPrincipal = Token.ToClaimsPrincipal(); - - var licenseKey = claimsPrincipal.GetValue(nameof(LicenseKey)); - if (licenseKey != user.LicenseKey) - { - return false; - } - - var premium = claimsPrincipal.GetValue(nameof(Premium)); - if (premium != user.Premium) - { - return false; - } - - var email = claimsPrincipal.GetValue(nameof(Email)); - if (!email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase)) - { - return false; - } + var licenseKey = ClaimsPrincipal.GetValue(nameof(LicenseKey)); + var premium = ClaimsPrincipal.GetValue(nameof(Premium)); + var email = ClaimsPrincipal.GetValue(nameof(Email)); - return true; + return licenseKey == user.LicenseKey && + premium == user.Premium && + email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase); } /// /// Do not extend this method. It is only here for backwards compatibility with old licenses. - /// Instead, extend the CanUse method using the ClaimsPrincipal. + /// Instead, extend the VerifyData method using the ClaimsPrincipal. /// /// /// /// private bool ObsoleteVerifyData(User user) { + // Do not extend this method. It is only here for backwards compatibility with old licenses. if (Issued > DateTime.UtcNow || Expires < DateTime.UtcNow) { return false; diff --git a/src/Core/Services/ILicensingService.cs b/src/Core/Services/ILicensingService.cs index 7458947d0efa..1f15e029296e 100644 --- a/src/Core/Services/ILicensingService.cs +++ b/src/Core/Services/ILicensingService.cs @@ -1,4 +1,5 @@ -using Bit.Core.AdminConsole.Entities; +using System.Security.Claims; +using Bit.Core.AdminConsole.Entities; using Bit.Core.Entities; using Bit.Core.Models.Business; @@ -13,6 +14,7 @@ public interface ILicensingService byte[] SignLicense(ILicense license); Task ReadOrganizationLicenseAsync(Organization organization); Task ReadOrganizationLicenseAsync(Guid organizationId); + ClaimsPrincipal GetClaimsPrincipalFromToken(string token, string audience); Task CreateOrganizationTokenAsync( Organization organization, diff --git a/src/Core/Services/Implementations/LicensingService.cs b/src/Core/Services/Implementations/LicensingService.cs index d088ea66a558..4089903c17a4 100644 --- a/src/Core/Services/Implementations/LicensingService.cs +++ b/src/Core/Services/Implementations/LicensingService.cs @@ -117,12 +117,6 @@ public async Task ValidateOrganizationsAsync() continue; } - if (!string.IsNullOrWhiteSpace(license.Token) && !VerifyToken(license.Token)) - { - await DisableOrganizationAsync(org, license, "Invalid token."); - continue; - } - if (!license.VerifyData(org, _globalSettings)) { await DisableOrganizationAsync(org, license, "Invalid data."); @@ -253,9 +247,17 @@ private async Task DisablePremiumAsync(User user, ILicense license, string reaso public bool VerifyLicense(ILicense license) { - return string.IsNullOrWhiteSpace(license.Token) - ? license.VerifySignature(_certificate) - : VerifyToken(license.Token); + if (string.IsNullOrWhiteSpace(license.Token)) + { + return license.VerifySignature(_certificate); + } + + return license switch + { + OrganizationLicense orgLicense => VerifyToken(license.Token, $"organization:{orgLicense.Id}"), + UserLicense userLicense => VerifyToken(license.Token, $"user:{userLicense.Id}"), + _ => throw new ArgumentException("Unsupported license type.", nameof(license)), + }; } public byte[] SignLicense(ILicense license) @@ -294,6 +296,32 @@ public async Task ReadOrganizationLicenseAsync(Guid organiz return await JsonSerializer.DeserializeAsync(fs); } + public ClaimsPrincipal GetClaimsPrincipalFromToken(string token, string audience) + { + var tokenHandler = new JwtSecurityTokenHandler(); + var validationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new X509SecurityKey(_certificate), + ValidateIssuer = true, + ValidIssuer = "bitwarden", + ValidateAudience = true, + ValidAudience = audience, + ValidateLifetime = true, + ClockSkew = TimeSpan.Zero, + RequireExpirationTime = true + }; + + try + { + return tokenHandler.ValidateToken(token, validationParameters, out _); + } + catch (Exception ex) + { + throw new SecurityTokenValidationException("License token validation failed.", ex); + } + } + public async Task CreateOrganizationTokenAsync(Organization organization, Guid installationId, SubscriptionInfo subscriptionInfo) { if (!_featureService.IsEnabled(FeatureFlagKeys.SelfHostLicenseRefactor)) @@ -352,11 +380,11 @@ private string GenerateToken(List claims, string audience, DateTime expir return tokenHandler.WriteToken(token); } - private bool VerifyToken(string token) + private bool VerifyToken(string token, string audience) { try { - _ = token.ToClaimsPrincipal(); + _ = GetClaimsPrincipalFromToken(token, audience); return true; } catch (Exception e) diff --git a/src/Core/Services/NoopImplementations/NoopLicensingService.cs b/src/Core/Services/NoopImplementations/NoopLicensingService.cs index 288840179649..51804d3bd9d8 100644 --- a/src/Core/Services/NoopImplementations/NoopLicensingService.cs +++ b/src/Core/Services/NoopImplementations/NoopLicensingService.cs @@ -1,4 +1,5 @@ -using Bit.Core.AdminConsole.Entities; +using System.Security.Claims; +using Bit.Core.AdminConsole.Entities; using Bit.Core.Entities; using Bit.Core.Models.Business; using Bit.Core.Settings; @@ -54,6 +55,11 @@ public Task ReadOrganizationLicenseAsync(Guid organizationI return Task.FromResult(null); } + public ClaimsPrincipal GetClaimsPrincipalFromToken(string token, string audience) + { + return null; + } + public Task CreateOrganizationTokenAsync(Organization organization, Guid installationId, SubscriptionInfo subscriptionInfo) { return Task.FromResult(null); diff --git a/test/Core.Test/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs index 0a6c98a2a8d9..0b5a971cbbe8 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs @@ -79,10 +79,11 @@ await sutProvider.GetDependency() .Received(1) .ReplaceAndUpdateCacheAsync(Arg.Is( org => AssertPropertyEqual(license, org, - "Id", "MaxStorageGb", "Issued", "Refresh", "Version", "Trial", "LicenseType", - "Hash", "Signature", "SignatureBytes", "InstallationId", "Expires", "ExpirationWithoutGracePeriod", "Token") && - // Same property but different name, use explicit mapping - org.ExpirationDate == license.Expires)); + "Id", "MaxStorageGb", "Issued", "Refresh", "Version", "Trial", "LicenseType", + "Hash", "Signature", "SignatureBytes", "InstallationId", "Expires", + "ExpirationWithoutGracePeriod", "Token", "ClaimsPrincipal") && + // Same property but different name, use explicit mapping + org.ExpirationDate == license.Expires)); } finally { From d366592446145a7bf8af66885ad2f585f7827713 Mon Sep 17 00:00:00 2001 From: Conner Turnbull Date: Thu, 7 Nov 2024 15:36:46 -0500 Subject: [PATCH 06/12] Moved creation of ClaimsPrincipal outside of licenses given dependecy on cert --- .../Implementations/OrganizationService.cs | 3 +- src/Core/Models/Business/ILicense.cs | 1 - .../Models/Business/OrganizationLicense.cs | 80 ++++++++++--------- src/Core/Models/Business/UserLicense.cs | 24 +++--- .../UpdateOrganizationLicenseCommand.cs | 3 +- src/Core/Services/ILicensingService.cs | 2 +- .../Implementations/LicensingService.cs | 48 ++++++----- .../Services/Implementations/UserService.cs | 8 +- .../NoopLicensingService.cs | 2 +- .../Business/OrganizationLicenseTests.cs | 7 +- .../UpdateOrganizationLicenseCommandTests.cs | 8 +- test/Core.Test/Services/UserServiceTests.cs | 6 +- 12 files changed, 103 insertions(+), 89 deletions(-) diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index f44ce686f4ed..9d94dea16c81 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -648,7 +648,8 @@ private async Task ValidateSignUpPoliciesAsync(Guid ownerId) OrganizationLicense license, User owner, string ownerKey, string collectionName, string publicKey, string privateKey) { - var canUse = license.CanUse(_globalSettings, _licensingService, out var exception); + var claimsPrincipal = _licensingService.GetClaimsPrincipalFromLicense(license); + var canUse = license.CanUse(_globalSettings, _licensingService, claimsPrincipal, out var exception); if (!canUse) { throw new BadRequestException(exception); diff --git a/src/Core/Models/Business/ILicense.cs b/src/Core/Models/Business/ILicense.cs index b1c2720b0534..87af0a3ee4b9 100644 --- a/src/Core/Models/Business/ILicense.cs +++ b/src/Core/Models/Business/ILicense.cs @@ -15,7 +15,6 @@ public interface ILicense string Signature { get; set; } string Token { get; set; } byte[] SignatureBytes { get; } - ClaimsPrincipal ClaimsPrincipal { get; set; } byte[] GetDataBytes(bool forHash = false); byte[] ComputeHash(); bool VerifySignature(X509Certificate2 certificate); diff --git a/src/Core/Models/Business/OrganizationLicense.cs b/src/Core/Models/Business/OrganizationLicense.cs index 8d437ae0f5d3..37a086646c64 100644 --- a/src/Core/Models/Business/OrganizationLicense.cs +++ b/src/Core/Models/Business/OrganizationLicense.cs @@ -106,9 +106,6 @@ public OrganizationLicense(Organization org, SubscriptionInfo subscriptionInfo, Hash = Convert.ToBase64String(ComputeHash()); Signature = Convert.ToBase64String(licenseService.SignLicense(this)); - ClaimsPrincipal = string.IsNullOrWhiteSpace(Token) - ? null - : licenseService.GetClaimsPrincipalFromToken(Token, $"organization:{Id}"); } public string LicenseKey { get; set; } @@ -158,7 +155,6 @@ public OrganizationLicense(Organization org, SubscriptionInfo subscriptionInfo, public string Signature { get; set; } public string Token { get; set; } [JsonIgnore] public byte[] SignatureBytes => Convert.FromBase64String(Signature); - [JsonIgnore] public ClaimsPrincipal ClaimsPrincipal { get; set; } /// /// Represents the current version of the license format. Should be updated whenever new fields are added. @@ -184,7 +180,6 @@ public byte[] GetDataBytes(bool forHash = false) !p.Name.Equals(nameof(SignatureBytes)) && !p.Name.Equals(nameof(LicenseType)) && !p.Name.Equals(nameof(Token)) && - !p.Name.Equals(nameof(ClaimsPrincipal)) && // UsersGetPremium was added in Version 2 (Version >= 2 || !p.Name.Equals(nameof(UsersGetPremium))) && // UseEvents was added in Version 3 @@ -245,34 +240,38 @@ public byte[] ComputeHash() } } - public bool CanUse(IGlobalSettings globalSettings, ILicensingService licensingService, out string exception) + public bool CanUse( + IGlobalSettings globalSettings, + ILicensingService licensingService, + ClaimsPrincipal claimsPrincipal, + out string exception) { - if (string.IsNullOrWhiteSpace(Token)) + if (string.IsNullOrWhiteSpace(Token) || claimsPrincipal is null) { return ObsoleteCanUse(globalSettings, licensingService, out exception); } var errorMessages = new StringBuilder(); - var enabled = ClaimsPrincipal.GetValue(nameof(Enabled)); + var enabled = claimsPrincipal.GetValue(nameof(Enabled)); if (!enabled) { errorMessages.AppendLine("Your cloud-hosted organization is currently disabled."); } - var installationId = ClaimsPrincipal.GetValue(nameof(InstallationId)); + var installationId = claimsPrincipal.GetValue(nameof(InstallationId)); if (installationId != globalSettings.Installation.Id) { errorMessages.AppendLine("The installation ID does not match the current installation."); } - var selfHost = ClaimsPrincipal.GetValue(nameof(SelfHost)); + var selfHost = claimsPrincipal.GetValue(nameof(SelfHost)); if (!selfHost) { errorMessages.AppendLine("The license does not allow for on-premise hosting of organizations."); } - var licenseType = ClaimsPrincipal.GetValue(nameof(LicenseType)); + var licenseType = claimsPrincipal.GetValue(nameof(LicenseType)); if (licenseType != Enums.LicenseType.Organization) { errorMessages.AppendLine("Premium licenses cannot be applied to an organization. " + @@ -353,40 +352,43 @@ private bool ObsoleteCanUse(IGlobalSettings globalSettings, ILicensingService li return true; } - public bool VerifyData(Organization organization, IGlobalSettings globalSettings) + public bool VerifyData( + Organization organization, + ClaimsPrincipal claimsPrincipal, + IGlobalSettings globalSettings) { if (string.IsNullOrWhiteSpace(Token)) { return ObsoleteVerifyData(organization, globalSettings); } - var issued = ClaimsPrincipal.GetValue(nameof(Issued)); - var expires = ClaimsPrincipal.GetValue(nameof(Expires)); - var installationId = ClaimsPrincipal.GetValue(nameof(InstallationId)); - var licenseKey = ClaimsPrincipal.GetValue(nameof(LicenseKey)); - var enabled = ClaimsPrincipal.GetValue(nameof(Enabled)); - var planType = ClaimsPrincipal.GetValue(nameof(PlanType)); - var seats = ClaimsPrincipal.GetValue(nameof(Seats)); - var maxCollections = ClaimsPrincipal.GetValue(nameof(MaxCollections)); - var useGroups = ClaimsPrincipal.GetValue(nameof(UseGroups)); - var useDirectory = ClaimsPrincipal.GetValue(nameof(UseDirectory)); - var useTotp = ClaimsPrincipal.GetValue(nameof(UseTotp)); - var selfHost = ClaimsPrincipal.GetValue(nameof(SelfHost)); - var name = ClaimsPrincipal.GetValue(nameof(Name)); - var usersGetPremium = ClaimsPrincipal.GetValue(nameof(UsersGetPremium)); - var useEvents = ClaimsPrincipal.GetValue(nameof(UseEvents)); - var use2fa = ClaimsPrincipal.GetValue(nameof(Use2fa)); - var useApi = ClaimsPrincipal.GetValue(nameof(UseApi)); - var usePolicies = ClaimsPrincipal.GetValue(nameof(UsePolicies)); - var useSso = ClaimsPrincipal.GetValue(nameof(UseSso)); - var useResetPassword = ClaimsPrincipal.GetValue(nameof(UseResetPassword)); - var useKeyConnector = ClaimsPrincipal.GetValue(nameof(UseKeyConnector)); - var useScim = ClaimsPrincipal.GetValue(nameof(UseScim)); - var useCustomPermissions = ClaimsPrincipal.GetValue(nameof(UseCustomPermissions)); - var useSecretsManager = ClaimsPrincipal.GetValue(nameof(UseSecretsManager)); - var usePasswordManager = ClaimsPrincipal.GetValue(nameof(UsePasswordManager)); - var smSeats = ClaimsPrincipal.GetValue(nameof(SmSeats)); - var smServiceAccounts = ClaimsPrincipal.GetValue(nameof(SmServiceAccounts)); + var issued = claimsPrincipal.GetValue(nameof(Issued)); + var expires = claimsPrincipal.GetValue(nameof(Expires)); + var installationId = claimsPrincipal.GetValue(nameof(InstallationId)); + var licenseKey = claimsPrincipal.GetValue(nameof(LicenseKey)); + var enabled = claimsPrincipal.GetValue(nameof(Enabled)); + var planType = claimsPrincipal.GetValue(nameof(PlanType)); + var seats = claimsPrincipal.GetValue(nameof(Seats)); + var maxCollections = claimsPrincipal.GetValue(nameof(MaxCollections)); + var useGroups = claimsPrincipal.GetValue(nameof(UseGroups)); + var useDirectory = claimsPrincipal.GetValue(nameof(UseDirectory)); + var useTotp = claimsPrincipal.GetValue(nameof(UseTotp)); + var selfHost = claimsPrincipal.GetValue(nameof(SelfHost)); + var name = claimsPrincipal.GetValue(nameof(Name)); + var usersGetPremium = claimsPrincipal.GetValue(nameof(UsersGetPremium)); + var useEvents = claimsPrincipal.GetValue(nameof(UseEvents)); + var use2fa = claimsPrincipal.GetValue(nameof(Use2fa)); + var useApi = claimsPrincipal.GetValue(nameof(UseApi)); + var usePolicies = claimsPrincipal.GetValue(nameof(UsePolicies)); + var useSso = claimsPrincipal.GetValue(nameof(UseSso)); + var useResetPassword = claimsPrincipal.GetValue(nameof(UseResetPassword)); + var useKeyConnector = claimsPrincipal.GetValue(nameof(UseKeyConnector)); + var useScim = claimsPrincipal.GetValue(nameof(UseScim)); + var useCustomPermissions = claimsPrincipal.GetValue(nameof(UseCustomPermissions)); + var useSecretsManager = claimsPrincipal.GetValue(nameof(UseSecretsManager)); + var usePasswordManager = claimsPrincipal.GetValue(nameof(UsePasswordManager)); + var smSeats = claimsPrincipal.GetValue(nameof(SmSeats)); + var smServiceAccounts = claimsPrincipal.GetValue(nameof(SmServiceAccounts)); return issued <= DateTime.UtcNow && expires >= DateTime.UtcNow && diff --git a/src/Core/Models/Business/UserLicense.cs b/src/Core/Models/Business/UserLicense.cs index 236e5e6a4793..41b1c43cdd09 100644 --- a/src/Core/Models/Business/UserLicense.cs +++ b/src/Core/Models/Business/UserLicense.cs @@ -56,9 +56,6 @@ public UserLicense(User user, ILicensingService licenseService, int? version = n Hash = Convert.ToBase64String(ComputeHash()); Signature = Convert.ToBase64String(licenseService.SignLicense(this)); - ClaimsPrincipal = string.IsNullOrWhiteSpace(Token) - ? null - : licenseService.GetClaimsPrincipalFromToken(Token, $"user:{Id}"); } public string LicenseKey { get; set; } @@ -78,8 +75,6 @@ public UserLicense(User user, ILicensingService licenseService, int? version = n public string Token { get; set; } [JsonIgnore] public byte[] SignatureBytes => Convert.FromBase64String(Signature); - [JsonIgnore] - public ClaimsPrincipal ClaimsPrincipal { get; set; } public byte[] GetDataBytes(bool forHash = false) { @@ -93,7 +88,6 @@ public byte[] GetDataBytes(bool forHash = false) !p.Name.Equals(nameof(SignatureBytes)) && !p.Name.Equals(nameof(LicenseType)) && !p.Name.Equals(nameof(Token)) && - !p.Name.Equals(nameof(ClaimsPrincipal)) && ( !forHash || ( @@ -123,22 +117,22 @@ public byte[] ComputeHash() } } - public bool CanUse(User user, out string exception) + public bool CanUse(User user, ClaimsPrincipal claimsPrincipal, out string exception) { - if (string.IsNullOrWhiteSpace(Token)) + if (string.IsNullOrWhiteSpace(Token) || claimsPrincipal is null) { return ObsoleteCanUse(user, out exception); } var errorMessages = new StringBuilder(); - var emailVerified = ClaimsPrincipal.GetValue(nameof(User.EmailVerified)); + var emailVerified = claimsPrincipal.GetValue(nameof(User.EmailVerified)); if (!emailVerified) { errorMessages.AppendLine("The user's email is not verified."); } - var email = ClaimsPrincipal.GetValue(nameof(Email)); + var email = claimsPrincipal.GetValue(nameof(Email)); if (!email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase)) { errorMessages.AppendLine("The user's email does not match the license email."); @@ -202,16 +196,16 @@ private bool ObsoleteCanUse(User user, out string exception) return true; } - public bool VerifyData(User user) + public bool VerifyData(User user, ClaimsPrincipal claimsPrincipal) { - if (string.IsNullOrWhiteSpace(Token)) + if (string.IsNullOrWhiteSpace(Token) || claimsPrincipal is null) { return ObsoleteVerifyData(user); } - var licenseKey = ClaimsPrincipal.GetValue(nameof(LicenseKey)); - var premium = ClaimsPrincipal.GetValue(nameof(Premium)); - var email = ClaimsPrincipal.GetValue(nameof(Email)); + var licenseKey = claimsPrincipal.GetValue(nameof(LicenseKey)); + var premium = claimsPrincipal.GetValue(nameof(Premium)); + var email = claimsPrincipal.GetValue(nameof(Email)); return licenseKey == user.LicenseKey && premium == user.Premium && diff --git a/src/Core/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommand.cs b/src/Core/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommand.cs index 1f8c6604b8f2..ffeee39c071b 100644 --- a/src/Core/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommand.cs @@ -39,7 +39,8 @@ public async Task UpdateLicenseAsync(SelfHostedOrganizationDetails selfHostedOrg throw new BadRequestException("License is already in use by another organization."); } - var canUse = license.CanUse(_globalSettings, _licensingService, out var exception) && + var claimsPrincipal = _licensingService.GetClaimsPrincipalFromLicense(license); + var canUse = license.CanUse(_globalSettings, _licensingService, claimsPrincipal, out var exception) && selfHostedOrganization.CanUseLicense(license, out exception); if (!canUse) diff --git a/src/Core/Services/ILicensingService.cs b/src/Core/Services/ILicensingService.cs index 1f15e029296e..7301f7c6898e 100644 --- a/src/Core/Services/ILicensingService.cs +++ b/src/Core/Services/ILicensingService.cs @@ -14,7 +14,7 @@ public interface ILicensingService byte[] SignLicense(ILicense license); Task ReadOrganizationLicenseAsync(Organization organization); Task ReadOrganizationLicenseAsync(Guid organizationId); - ClaimsPrincipal GetClaimsPrincipalFromToken(string token, string audience); + ClaimsPrincipal GetClaimsPrincipalFromLicense(ILicense license); Task CreateOrganizationTokenAsync( Organization organization, diff --git a/src/Core/Services/Implementations/LicensingService.cs b/src/Core/Services/Implementations/LicensingService.cs index 4089903c17a4..c83ab952f702 100644 --- a/src/Core/Services/Implementations/LicensingService.cs +++ b/src/Core/Services/Implementations/LicensingService.cs @@ -117,7 +117,7 @@ public async Task ValidateOrganizationsAsync() continue; } - if (!license.VerifyData(org, _globalSettings)) + if (!license.VerifyData(org, GetClaimsPrincipalFromLicense(license), _globalSettings)) { await DisableOrganizationAsync(org, license, "Invalid data."); continue; @@ -216,7 +216,8 @@ private async Task ProcessUserValidationAsync(User user) return false; } - if (!license.VerifyData(user)) + var claimsPrincipal = GetClaimsPrincipalFromLicense(license); + if (!license.VerifyData(user, claimsPrincipal)) { await DisablePremiumAsync(user, license, "Invalid data."); return false; @@ -252,12 +253,16 @@ public bool VerifyLicense(ILicense license) return license.VerifySignature(_certificate); } - return license switch + try { - OrganizationLicense orgLicense => VerifyToken(license.Token, $"organization:{orgLicense.Id}"), - UserLicense userLicense => VerifyToken(license.Token, $"user:{userLicense.Id}"), - _ => throw new ArgumentException("Unsupported license type.", nameof(license)), - }; + _ = GetClaimsPrincipalFromLicense(license); + return true; + } + catch (Exception e) + { + _logger.LogWarning(e, "Invalid token."); + return false; + } } public byte[] SignLicense(ILicense license) @@ -296,8 +301,21 @@ public async Task ReadOrganizationLicenseAsync(Guid organiz return await JsonSerializer.DeserializeAsync(fs); } - public ClaimsPrincipal GetClaimsPrincipalFromToken(string token, string audience) + public ClaimsPrincipal GetClaimsPrincipalFromLicense(ILicense license) { + if (string.IsNullOrWhiteSpace(license.Token)) + { + return null; + } + + var audience = license switch + { + OrganizationLicense orgLicense => $"organization:{orgLicense.Id}", + UserLicense userLicense => $"user:{userLicense.Id}", + _ => throw new ArgumentException("Unsupported license type.", nameof(license)), + }; + + var token = license.Token; var tokenHandler = new JwtSecurityTokenHandler(); var validationParameters = new TokenValidationParameters { @@ -379,18 +397,4 @@ private string GenerateToken(List claims, string audience, DateTime expir var token = tokenHandler.CreateToken(tokenDescriptor); return tokenHandler.WriteToken(token); } - - private bool VerifyToken(string token, string audience) - { - try - { - _ = GetClaimsPrincipalFromToken(token, audience); - return true; - } - catch (Exception e) - { - _logger.LogWarning(e, "Invalid token."); - return false; - } - } } diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 948a580ab0c1..fa8cd3cef855 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -908,7 +908,9 @@ public async Task> SignUpPremiumAsync(User user, string paym throw new BadRequestException("Invalid license."); } - if (!license.CanUse(user, out var exceptionMessage)) + var claimsPrincipal = _licenseService.GetClaimsPrincipalFromLicense(license); + + if (!license.CanUse(user, claimsPrincipal, out var exceptionMessage)) { throw new BadRequestException(exceptionMessage); } @@ -987,7 +989,9 @@ public async Task UpdateLicenseAsync(User user, UserLicense license) throw new BadRequestException("Invalid license."); } - if (!license.CanUse(user, out var exceptionMessage)) + var claimsPrincipal = _licenseService.GetClaimsPrincipalFromLicense(license); + + if (!license.CanUse(user, claimsPrincipal, out var exceptionMessage)) { throw new BadRequestException(exceptionMessage); } diff --git a/src/Core/Services/NoopImplementations/NoopLicensingService.cs b/src/Core/Services/NoopImplementations/NoopLicensingService.cs index 51804d3bd9d8..dc733e9a3387 100644 --- a/src/Core/Services/NoopImplementations/NoopLicensingService.cs +++ b/src/Core/Services/NoopImplementations/NoopLicensingService.cs @@ -55,7 +55,7 @@ public Task ReadOrganizationLicenseAsync(Guid organizationI return Task.FromResult(null); } - public ClaimsPrincipal GetClaimsPrincipalFromToken(string token, string audience) + public ClaimsPrincipal GetClaimsPrincipalFromLicense(ILicense license) { return null; } diff --git a/test/Core.Test/Models/Business/OrganizationLicenseTests.cs b/test/Core.Test/Models/Business/OrganizationLicenseTests.cs index c2eb0dd93422..26945f533e84 100644 --- a/test/Core.Test/Models/Business/OrganizationLicenseTests.cs +++ b/test/Core.Test/Models/Business/OrganizationLicenseTests.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Security.Claims; +using System.Text.Json; using Bit.Core.Models.Business; using Bit.Core.Services; using Bit.Core.Settings; @@ -36,7 +37,7 @@ public void OrganizationLicense_LoadFromDisk_HashDoesNotChange(int licenseVersio [Theory] [BitAutoData(OrganizationLicense.CurrentLicenseFileVersion)] // Previous version (this property is 1 behind) [BitAutoData(OrganizationLicense.CurrentLicenseFileVersion + 1)] // Current version - public void OrganizationLicense_LoadedFromDisk_VerifyData_Passes(int licenseVersion) + public void OrganizationLicense_LoadedFromDisk_VerifyData_Passes(int licenseVersion, ClaimsPrincipal claimsPrincipal) { var license = OrganizationLicenseFileFixtures.GetVersion(licenseVersion); @@ -49,7 +50,7 @@ public void OrganizationLicense_LoadedFromDisk_VerifyData_Passes(int licenseVers { Id = new Guid(OrganizationLicenseFileFixtures.InstallationId) }); - Assert.True(license.VerifyData(organization, globalSettings)); + Assert.True(license.VerifyData(organization, claimsPrincipal, globalSettings)); } /// diff --git a/test/Core.Test/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs index 0b5a971cbbe8..b8e677177c0c 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationLicenses/UpdateOrganizationLicenseCommandTests.cs @@ -1,4 +1,5 @@ -using Bit.Core.AdminConsole.Entities; +using System.Security.Claims; +using Bit.Core.AdminConsole.Entities; using Bit.Core.Enums; using Bit.Core.Models.Business; using Bit.Core.Models.Data.Organizations; @@ -48,6 +49,9 @@ public async Task UpdateLicenseAsync_UpdatesLicenseFileAndOrganization( license.InstallationId = globalSettings.Installation.Id; license.LicenseType = LicenseType.Organization; sutProvider.GetDependency().VerifyLicense(license).Returns(true); + sutProvider.GetDependency() + .GetClaimsPrincipalFromLicense(license) + .Returns((ClaimsPrincipal)null); // Passing values for SelfHostedOrganizationDetails.CanUseLicense // NSubstitute cannot override non-virtual members so we have to ensure the real method passes @@ -81,7 +85,7 @@ await sutProvider.GetDependency() org => AssertPropertyEqual(license, org, "Id", "MaxStorageGb", "Issued", "Refresh", "Version", "Trial", "LicenseType", "Hash", "Signature", "SignatureBytes", "InstallationId", "Expires", - "ExpirationWithoutGracePeriod", "Token", "ClaimsPrincipal") && + "ExpirationWithoutGracePeriod", "Token") && // Same property but different name, use explicit mapping org.ExpirationDate == license.Expires)); } diff --git a/test/Core.Test/Services/UserServiceTests.cs b/test/Core.Test/Services/UserServiceTests.cs index aa2c0a5cc97c..71cceb86ad08 100644 --- a/test/Core.Test/Services/UserServiceTests.cs +++ b/test/Core.Test/Services/UserServiceTests.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Security.Claims; +using System.Text.Json; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; @@ -62,6 +63,9 @@ public async Task UpdateLicenseAsync_Success(SutProvider sutProvide sutProvider.GetDependency() .VerifyLicense(userLicense) .Returns(true); + sutProvider.GetDependency() + .GetClaimsPrincipalFromLicense(userLicense) + .Returns((ClaimsPrincipal)null); await sutProvider.Sut.UpdateLicenseAsync(user, userLicense); From 8cd040641fcbc7a7e9c5a03a535f5e31d28aec3d Mon Sep 17 00:00:00 2001 From: Conner Turnbull Date: Thu, 7 Nov 2024 16:06:20 -0500 Subject: [PATCH 07/12] Ran dotnet format. Resolved identity error --- src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs | 5 +---- src/Core/Models/Business/ILicense.cs | 3 +-- src/Core/Services/Implementations/LicensingService.cs | 5 ++++- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs b/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs index 92bfccdeda6c..184d8dad23a1 100644 --- a/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs +++ b/src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs @@ -1,10 +1,7 @@ -using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; -using System.Security.Cryptography.X509Certificates; +using System.Security.Claims; using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Models.Business; -using Microsoft.IdentityModel.Tokens; namespace Bit.Core.Billing.Licenses.Extensions; diff --git a/src/Core/Models/Business/ILicense.cs b/src/Core/Models/Business/ILicense.cs index 87af0a3ee4b9..b0e295bdd987 100644 --- a/src/Core/Models/Business/ILicense.cs +++ b/src/Core/Models/Business/ILicense.cs @@ -1,5 +1,4 @@ -using System.Security.Claims; -using System.Security.Cryptography.X509Certificates; +using System.Security.Cryptography.X509Certificates; namespace Bit.Core.Models.Business; diff --git a/src/Core/Services/Implementations/LicensingService.cs b/src/Core/Services/Implementations/LicensingService.cs index c83ab952f702..1ff65383465a 100644 --- a/src/Core/Services/Implementations/LicensingService.cs +++ b/src/Core/Services/Implementations/LicensingService.cs @@ -8,6 +8,7 @@ using Bit.Core.Billing.Licenses.Models; using Bit.Core.Billing.Licenses.Services; using Bit.Core.Entities; +using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.Repositories; using Bit.Core.Settings; @@ -336,7 +337,9 @@ public ClaimsPrincipal GetClaimsPrincipalFromLicense(ILicense license) } catch (Exception ex) { - throw new SecurityTokenValidationException("License token validation failed.", ex); + // Token exceptions thrown are interpreted by the client as Identity errors and cause the user to logout + // Mask them by rethrowing as BadRequestException + throw new BadRequestException($"Invalid license. {ex.Message}"); } } From d56de0715bec4291bbd68be60f53753dc4e2937a Mon Sep 17 00:00:00 2001 From: Conner Turnbull Date: Thu, 7 Nov 2024 16:18:10 -0500 Subject: [PATCH 08/12] Updated claim types to use string constants --- src/Core/Billing/Licenses/LicenseConstants.cs | 58 ++++++++++++++ .../OrganizationLicenseClaimsFactory.cs | 80 +++++++++---------- .../UserLicenseClaimsFactory.cs | 23 +++--- 3 files changed, 107 insertions(+), 54 deletions(-) create mode 100644 src/Core/Billing/Licenses/LicenseConstants.cs diff --git a/src/Core/Billing/Licenses/LicenseConstants.cs b/src/Core/Billing/Licenses/LicenseConstants.cs new file mode 100644 index 000000000000..9e62fafedfe6 --- /dev/null +++ b/src/Core/Billing/Licenses/LicenseConstants.cs @@ -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); +} diff --git a/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs b/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs index 8f6a87c2c2b2..300b87dcca0a 100644 --- a/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs +++ b/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs @@ -21,52 +21,48 @@ public Task> GenerateClaims(Organization entity, LicenseContext lice var claims = new List { - 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()), + 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.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)); + claims.Add(new Claim(nameof(OrganizationLicenseConstants.BusinessName), entity.BusinessName)); } return Task.FromResult(claims); diff --git a/src/Core/Billing/Licenses/Services/Implementations/UserLicenseClaimsFactory.cs b/src/Core/Billing/Licenses/Services/Implementations/UserLicenseClaimsFactory.cs index 59e2bd57bcf1..28c779c3d6b5 100644 --- a/src/Core/Billing/Licenses/Services/Implementations/UserLicenseClaimsFactory.cs +++ b/src/Core/Billing/Licenses/Services/Implementations/UserLicenseClaimsFactory.cs @@ -3,7 +3,6 @@ using Bit.Core.Billing.Licenses.Models; using Bit.Core.Entities; using Bit.Core.Enums; -using Bit.Core.Models.Business; namespace Bit.Core.Billing.Licenses.Services.Implementations; @@ -20,17 +19,17 @@ public Task> GenerateClaims(User entity, LicenseContext licenseConte var claims = new List { - new(nameof(UserLicense.LicenseType), LicenseType.User.ToString()), - new(nameof(UserLicense.LicenseKey), entity.LicenseKey), - new(nameof(UserLicense.Id), entity.Id.ToString()), - new(nameof(UserLicense.Name), entity.Name), - new(nameof(UserLicense.Email), entity.Email), - new(nameof(UserLicense.Premium), entity.Premium.ToString()), - new(nameof(UserLicense.MaxStorageGb), entity.MaxStorageGb.ToString()), - new(nameof(UserLicense.Issued), DateTime.UtcNow.ToString(CultureInfo.InvariantCulture)), - new(nameof(UserLicense.Expires), expires.ToString()), - new(nameof(UserLicense.Refresh), refresh.ToString()), - new(nameof(UserLicense.Trial), trial.ToString()), + 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); From 6b63cb7818dfe2da851e937d6505d7909af3831b Mon Sep 17 00:00:00 2001 From: Conner Turnbull Date: Thu, 7 Nov 2024 16:20:36 -0500 Subject: [PATCH 09/12] Updated jwt expires to be one year --- src/Core/Services/Implementations/LicensingService.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Core/Services/Implementations/LicensingService.cs b/src/Core/Services/Implementations/LicensingService.cs index 1ff65383465a..450d7633d4d3 100644 --- a/src/Core/Services/Implementations/LicensingService.cs +++ b/src/Core/Services/Implementations/LicensingService.cs @@ -358,9 +358,8 @@ public async Task CreateOrganizationTokenAsync(Organization organization var claims = await _organizationLicenseClaimsFactory.GenerateClaims(organization, licenseContext); var audience = $"organization:{organization.Id}"; - var expires = organization.CalculateFreshExpirationDate(subscriptionInfo); - return GenerateToken(claims, audience, expires); + return GenerateToken(claims, audience); } public async Task CreateUserTokenAsync(User user, SubscriptionInfo subscriptionInfo) @@ -373,12 +372,11 @@ public async Task CreateUserTokenAsync(User user, SubscriptionInfo subsc var licenseContext = new LicenseContext { SubscriptionInfo = subscriptionInfo }; var claims = await _userLicenseClaimsFactory.GenerateClaims(user, licenseContext); var audience = $"user:{user.Id}"; - var expires = user.PremiumExpirationDate ?? DateTime.UtcNow.AddDays(7); - return GenerateToken(claims, audience, expires); + return GenerateToken(claims, audience); } - private string GenerateToken(List claims, string audience, DateTime expires) + private string GenerateToken(List claims, string audience) { if (claims.All(claim => claim.Type != JwtClaimTypes.JwtId)) { @@ -392,7 +390,7 @@ private string GenerateToken(List claims, string audience, DateTime expir Issuer = "bitwarden", Audience = audience, NotBefore = DateTime.UtcNow, - Expires = expires, + Expires = DateTime.UtcNow.AddYears(1), // Org expiration is a claim SigningCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.RsaSha256Signature) }; From 73aa5fb16d1973277e45b3fd883358ad96fd4676 Mon Sep 17 00:00:00 2001 From: Conner Turnbull Date: Thu, 7 Nov 2024 16:32:33 -0500 Subject: [PATCH 10/12] Fixed bug requiring email verification to be on the token --- src/Core/Models/Business/UserLicense.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Core/Models/Business/UserLicense.cs b/src/Core/Models/Business/UserLicense.cs index 41b1c43cdd09..797aa6692a8a 100644 --- a/src/Core/Models/Business/UserLicense.cs +++ b/src/Core/Models/Business/UserLicense.cs @@ -126,8 +126,7 @@ public bool CanUse(User user, ClaimsPrincipal claimsPrincipal, out string except var errorMessages = new StringBuilder(); - var emailVerified = claimsPrincipal.GetValue(nameof(User.EmailVerified)); - if (!emailVerified) + if (!user.EmailVerified) { errorMessages.AppendLine("The user's email is not verified."); } From 9790d2cef0342ce498bc53723e6415003c7759c0 Mon Sep 17 00:00:00 2001 From: Conner Turnbull Date: Fri, 8 Nov 2024 09:23:55 -0500 Subject: [PATCH 11/12] dotnet format --- src/Core/Billing/Licenses/LicenseConstants.cs | 2 +- src/Core/Services/Implementations/LicensingService.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Core/Billing/Licenses/LicenseConstants.cs b/src/Core/Billing/Licenses/LicenseConstants.cs index 9e62fafedfe6..564019affc3f 100644 --- a/src/Core/Billing/Licenses/LicenseConstants.cs +++ b/src/Core/Billing/Licenses/LicenseConstants.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Billing.Licenses; +namespace Bit.Core.Billing.Licenses; public static class OrganizationLicenseConstants { diff --git a/src/Core/Services/Implementations/LicensingService.cs b/src/Core/Services/Implementations/LicensingService.cs index 450d7633d4d3..866f0bb6e1d8 100644 --- a/src/Core/Services/Implementations/LicensingService.cs +++ b/src/Core/Services/Implementations/LicensingService.cs @@ -4,7 +4,6 @@ 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; From 51537ab41e5194b67707c1d725f08fa317c735f7 Mon Sep 17 00:00:00 2001 From: Matt Bishop Date: Mon, 11 Nov 2024 16:30:37 -0500 Subject: [PATCH 12/12] Patch build process --- .github/workflows/build.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f83b03b166ef..1d092d8b4dcb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,6 +21,8 @@ jobs: lint: name: Lint runs-on: ubuntu-22.04 + needs: + - check-run steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -38,7 +40,6 @@ jobs: runs-on: ubuntu-22.04 needs: - lint - - check-run strategy: fail-fast: false matrix: @@ -132,7 +133,6 @@ jobs: security-events: write needs: - build-artifacts - - check-run strategy: fail-fast: false matrix: @@ -478,7 +478,8 @@ jobs: build-mssqlmigratorutility: name: Build MSSQL migrator utility runs-on: ubuntu-22.04 - needs: lint + needs: + - lint defaults: run: shell: bash @@ -531,7 +532,8 @@ jobs: name: Trigger self-host build if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main' runs-on: ubuntu-22.04 - needs: build-docker + needs: + - build-docker steps: - name: Log in to Azure - CI subscription uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -564,7 +566,8 @@ jobs: name: Trigger k8s deploy if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main' runs-on: ubuntu-22.04 - needs: build-docker + needs: + - build-docker steps: - name: Log in to Azure - CI subscription uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -600,7 +603,8 @@ jobs: github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'ephemeral-environment') runs-on: ubuntu-24.04 - needs: build-docker + needs: + - build-docker steps: - name: Log in to Azure - CI subscription uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0