1
0
mirror of https://github.com/bitwarden/server synced 2026-01-14 06:23:46 +00:00

[PM-29611] Decouple License from Subscription Response (#6768)

* implement the ticket request

* resolve the build lint error

* Resolve the build lint error

* Address review comments

* Fixt the lint and failing unit test

* Fix NSubstitute mock - use concrete ClaimsPrincipal instead of Arg.Any in Returns()

* resolve InjectUser issues

* Fix the failing testing

* Fix the failing unit test
This commit is contained in:
cyprain-okeke
2025-12-31 17:30:41 +01:00
committed by GitHub
parent bf08bd279c
commit 665be6bfb0
7 changed files with 164 additions and 0 deletions

View File

@@ -2,6 +2,7 @@
using Bit.Api.Billing.Models.Requests.Payment;
using Bit.Api.Billing.Models.Requests.Premium;
using Bit.Core;
using Bit.Core.Billing.Licenses.Queries;
using Bit.Core.Billing.Payment.Commands;
using Bit.Core.Billing.Payment.Queries;
using Bit.Core.Billing.Premium.Commands;
@@ -21,6 +22,7 @@ public class AccountBillingVNextController(
ICreatePremiumCloudHostedSubscriptionCommand createPremiumCloudHostedSubscriptionCommand,
IGetCreditQuery getCreditQuery,
IGetPaymentMethodQuery getPaymentMethodQuery,
IGetUserLicenseQuery getUserLicenseQuery,
IUpdatePaymentMethodCommand updatePaymentMethodCommand) : BaseBillingController
{
[HttpGet("credit")]
@@ -77,4 +79,13 @@ public class AccountBillingVNextController(
user, paymentMethod, billingAddress, additionalStorageGb);
return Handle(result);
}
[HttpGet("license")]
[InjectUser]
public async Task<IResult> GetLicenseAsync(
[BindNever] User user)
{
var response = await getUserLicenseQuery.Run(user);
return TypedResults.Ok(response);
}
}

View File

@@ -1,5 +1,6 @@
using Bit.Core.Billing.Caches;
using Bit.Core.Billing.Caches.Implementations;
using Bit.Core.Billing.Licenses;
using Bit.Core.Billing.Licenses.Extensions;
using Bit.Core.Billing.Organizations.Commands;
using Bit.Core.Billing.Organizations.Queries;
@@ -28,6 +29,7 @@ public static class ServiceCollectionExtensions
services.AddTransient<ISetupIntentCache, SetupIntentDistributedCache>();
services.AddTransient<ISubscriberService, SubscriberService>();
services.AddLicenseServices();
services.AddLicenseOperations();
services.AddPricingClient();
services.AddPaymentOperations();
services.AddOrganizationLicenseCommandsQueries();

View File

@@ -0,0 +1,44 @@
using System.Security.Claims;
using Bit.Core.Billing.Licenses.Extensions;
using Bit.Core.Billing.Models.Business;
using Bit.Core.Models.Api;
namespace Bit.Core.Billing.Licenses.Models.Api.Response;
/// <summary>
/// Response model containing user license information.
/// Separated from subscription data to maintain separation of concerns.
/// </summary>
public class LicenseResponseModel : ResponseModel
{
public LicenseResponseModel(UserLicense license, ClaimsPrincipal? claimsPrincipal)
: base("license")
{
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;
}
}
/// <summary>
/// The user's license containing feature entitlements and metadata.
/// </summary>
public UserLicense License { get; set; }
/// <summary>
/// The license expiration date.
/// Extracted from the cryptographically secured JWT token when available,
/// otherwise falls back to the license file's expiration date.
/// </summary>
public DateTime? Expiration { get; set; }
}

View File

@@ -0,0 +1,23 @@
using Bit.Core.Billing.Licenses.Models.Api.Response;
using Bit.Core.Billing.Services;
using Bit.Core.Entities;
using Bit.Core.Services;
namespace Bit.Core.Billing.Licenses.Queries;
public interface IGetUserLicenseQuery
{
Task<LicenseResponseModel> Run(User user);
}
public class GetUserLicenseQuery(
IUserService userService,
ILicensingService licensingService) : IGetUserLicenseQuery
{
public async Task<LicenseResponseModel> Run(User user)
{
var license = await userService.GenerateLicenseAsync(user);
var claimsPrincipal = licensingService.GetClaimsPrincipalFromLicense(license);
return new LicenseResponseModel(license, claimsPrincipal);
}
}

View File

@@ -0,0 +1,13 @@
using Bit.Core.Billing.Licenses.Queries;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.Core.Billing.Licenses;
public static class Registrations
{
public static void AddLicenseOperations(this IServiceCollection services)
{
// Queries
services.AddTransient<IGetUserLicenseQuery, GetUserLicenseQuery>();
}
}