[PM-28508] Fix No validation occurs for Expiration date on Self Host licenses (#6655)

* Fix the license validation bug

* resolve the failing test

* fix the failing test

* Revert changes and Add the ui display fix

* remove empty spaces

* revert the changes on licensing file

* revert changes to the test signup

* Revert the org license file changes

* revert the empty spaces

* revert the empty spaces changes

* remove the empty spaces

* revert

* Remove the duplicate code

* Add the expire date fix for premium

* Fix the failing test

* Fix the lint error
This commit is contained in:
cyprain-okeke 2025-12-04 16:28:01 +01:00 committed by GitHub
parent 655054aa56
commit d619a49998
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 84 additions and 6 deletions

View File

@ -1,10 +1,13 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using System.Security.Claims;
using System.Text.Json.Serialization;
using Bit.Api.Models.Response;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Licenses;
using Bit.Core.Billing.Licenses.Extensions;
using Bit.Core.Billing.Organizations.Models;
using Bit.Core.Models.Api;
using Bit.Core.Models.Business;
@ -177,6 +180,30 @@ public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
}
}
public OrganizationSubscriptionResponseModel(Organization organization, OrganizationLicense license, ClaimsPrincipal claimsPrincipal) :
this(organization, (Plan)null)
{
if (license != null)
{
// CRITICAL: When a license has a Token (JWT), ALWAYS use the expiration from the token claim
// The token's expiration is cryptographically secured and cannot be tampered with
// The file's Expires property can be manually edited and should NOT be trusted for display
if (claimsPrincipal != null)
{
Expiration = claimsPrincipal.GetValue<DateTime>(OrganizationLicenseConstants.Expires);
ExpirationWithoutGracePeriod = claimsPrincipal.GetValue<DateTime?>(OrganizationLicenseConstants.ExpirationWithoutGracePeriod);
}
else
{
// No token - use the license file expiration (for older licenses without tokens)
Expiration = license.Expires;
ExpirationWithoutGracePeriod = license.ExpirationWithoutGracePeriod ?? (license.Trial
? license.Expires
: license.Expires?.AddDays(-Constants.OrganizationSelfHostSubscriptionGracePeriodDays));
}
}
}
public string StorageName { get; set; }
public double? StorageGb { get; set; }
public BillingCustomerDiscount CustomerDiscount { get; set; }

View File

@ -26,7 +26,8 @@ public class AccountsController(
IUserService userService,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IUserAccountKeysQuery userAccountKeysQuery,
IFeatureService featureService) : Controller
IFeatureService featureService,
ILicensingService licensingService) : Controller
{
[HttpPost("premium")]
public async Task<PaymentResponseModel> PostPremiumAsync(
@ -97,12 +98,14 @@ public class AccountsController(
var includeMilestone2Discount = featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2);
var subscriptionInfo = await paymentService.GetSubscriptionAsync(user);
var license = await userService.GenerateLicenseAsync(user, subscriptionInfo);
return new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount);
var claimsPrincipal = licensingService.GetClaimsPrincipalFromLicense(license);
return new SubscriptionResponseModel(user, subscriptionInfo, license, claimsPrincipal, includeMilestone2Discount);
}
else
{
var license = await userService.GenerateLicenseAsync(user);
return new SubscriptionResponseModel(user, license);
var claimsPrincipal = licensingService.GetClaimsPrincipalFromLicense(license);
return new SubscriptionResponseModel(user, null, license, claimsPrincipal);
}
}
else

View File

@ -67,7 +67,8 @@ public class OrganizationsController(
if (globalSettings.SelfHosted)
{
var orgLicense = await licensingService.ReadOrganizationLicenseAsync(organization);
return new OrganizationSubscriptionResponseModel(organization, orgLicense);
var claimsPrincipal = licensingService.GetClaimsPrincipalFromLicense(orgLicense);
return new OrganizationSubscriptionResponseModel(organization, orgLicense, claimsPrincipal);
}
var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);

View File

@ -1,4 +1,7 @@
using Bit.Core.Billing.Constants;
using System.Security.Claims;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Licenses;
using Bit.Core.Billing.Licenses.Extensions;
using Bit.Core.Billing.Models.Business;
using Bit.Core.Entities;
using Bit.Core.Models.Api;
@ -37,6 +40,46 @@ public class SubscriptionResponseModel : ResponseModel
: null;
}
/// <param name="user">The user entity containing storage and premium subscription information</param>
/// <param name="subscription">Subscription information retrieved from the payment provider (Stripe/Braintree)</param>
/// <param name="license">The user's license containing expiration and feature entitlements</param>
/// <param name="claimsPrincipal">The claims principal containing cryptographically secure token claims</param>
/// <param name="includeMilestone2Discount">
/// Whether to include discount information in the response.
/// Set to true when the PM23341_Milestone_2 feature flag is enabled AND
/// you want to expose Milestone 2 discount information to the client.
/// The discount will only be included if it matches the specific Milestone 2 coupon ID.
/// </param>
public SubscriptionResponseModel(User user, SubscriptionInfo? subscription, UserLicense license, ClaimsPrincipal? claimsPrincipal, bool includeMilestone2Discount = false)
: base("subscription")
{
Subscription = subscription?.Subscription != null ? new BillingSubscription(subscription.Subscription) : null;
UpcomingInvoice = subscription?.UpcomingInvoice != null ?
new BillingSubscriptionUpcomingInvoice(subscription.UpcomingInvoice) : null;
StorageName = user.Storage.HasValue ? CoreHelpers.ReadableBytesSize(user.Storage.Value) : null;
StorageGb = user.Storage.HasValue ? Math.Round(user.Storage.Value / 1073741824D, 2) : 0; // 1 GB
MaxStorageGb = user.MaxStorageGb;
License = license;
// CRITICAL: When a license has a Token (JWT), ALWAYS use the expiration from the token claim
// The token's expiration is cryptographically secured and cannot be tampered with
// The file's Expires property can be manually edited and should NOT be trusted for display
if (claimsPrincipal != null)
{
Expiration = claimsPrincipal.GetValue<DateTime?>(UserLicenseConstants.Expires);
}
else
{
// No token - use the license file expiration (for older licenses without tokens)
Expiration = License.Expires;
}
// Only display the Milestone 2 subscription discount on the subscription page.
CustomerDiscount = ShouldIncludeMilestone2Discount(includeMilestone2Discount, subscription?.CustomerDiscount)
? new BillingCustomerDiscount(subscription!.CustomerDiscount!)
: null;
}
public SubscriptionResponseModel(User user, UserLicense? license = null)
: base("subscription")
{

View File

@ -4,6 +4,7 @@ using Bit.Core;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Models.Business;
using Bit.Core.Billing.Services;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Queries.Interfaces;
@ -30,6 +31,7 @@ public class AccountsControllerTests : IDisposable
private readonly IPaymentService _paymentService;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
private readonly ILicensingService _licensingService;
private readonly GlobalSettings _globalSettings;
private readonly AccountsController _sut;
@ -40,13 +42,15 @@ public class AccountsControllerTests : IDisposable
_paymentService = Substitute.For<IPaymentService>();
_twoFactorIsEnabledQuery = Substitute.For<ITwoFactorIsEnabledQuery>();
_userAccountKeysQuery = Substitute.For<IUserAccountKeysQuery>();
_licensingService = Substitute.For<ILicensingService>();
_globalSettings = new GlobalSettings { SelfHosted = false };
_sut = new AccountsController(
_userService,
_twoFactorIsEnabledQuery,
_userAccountKeysQuery,
_featureService
_featureService,
_licensingService
);
}