mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 15:42:48 -05:00
[PM-11516] Initial license file refactor (#5002)
* Added the ability to create a JWT on an organization license that contains all license properties as claims * Added the ability to create a JWT on a user license that contains all license properties as claims * Added ability to consume JWT licenses * Resolved generic type issues when getting claim value * Now validating the jwt signature, exp, and iat * Moved creation of ClaimsPrincipal outside of licenses given dependecy on cert * Ran dotnet format. Resolved identity error * Updated claim types to use string constants * Updated jwt expires to be one year * Fixed bug requiring email verification to be on the token * dotnet format * Patch build process --------- Co-authored-by: Matt Bishop <mbishop@bitwarden.com>
This commit is contained in:
@ -1,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,5 +14,12 @@ public interface ILicensingService
|
||||
byte[] SignLicense(ILicense license);
|
||||
Task<OrganizationLicense> ReadOrganizationLicenseAsync(Organization organization);
|
||||
Task<OrganizationLicense> ReadOrganizationLicenseAsync(Guid organizationId);
|
||||
ClaimsPrincipal GetClaimsPrincipalFromLicense(ILicense license);
|
||||
|
||||
Task<string> CreateOrganizationTokenAsync(
|
||||
Organization organization,
|
||||
Guid installationId,
|
||||
SubscriptionInfo subscriptionInfo);
|
||||
|
||||
Task<string> CreateUserTokenAsync(User user, SubscriptionInfo subscriptionInfo);
|
||||
}
|
||||
|
@ -1,15 +1,22 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Licenses.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;
|
||||
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;
|
||||
|
||||
@ -19,27 +26,33 @@ 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<LicensingService> _logger;
|
||||
private readonly ILicenseClaimsFactory<Organization> _organizationLicenseClaimsFactory;
|
||||
private readonly ILicenseClaimsFactory<User> _userLicenseClaimsFactory;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
private IDictionary<Guid, DateTime> _userCheckCache = new Dictionary<Guid, DateTime>();
|
||||
|
||||
public LicensingService(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IMailService mailService,
|
||||
IWebHostEnvironment environment,
|
||||
ILogger<LicensingService> logger,
|
||||
IGlobalSettings globalSettings)
|
||||
IGlobalSettings globalSettings,
|
||||
ILicenseClaimsFactory<Organization> organizationLicenseClaimsFactory,
|
||||
IFeatureService featureService,
|
||||
ILicenseClaimsFactory<User> userLicenseClaimsFactory)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_mailService = mailService;
|
||||
_logger = logger;
|
||||
_globalSettings = globalSettings;
|
||||
_organizationLicenseClaimsFactory = organizationLicenseClaimsFactory;
|
||||
_featureService = featureService;
|
||||
_userLicenseClaimsFactory = userLicenseClaimsFactory;
|
||||
|
||||
var certThumbprint = environment.IsDevelopment() ?
|
||||
"207E64A231E8AA32AAF68A61037C075EBEBD553F" :
|
||||
@ -104,13 +117,13 @@ public class LicensingService : ILicensingService
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!license.VerifyData(org, _globalSettings))
|
||||
if (!license.VerifyData(org, GetClaimsPrincipalFromLicense(license), _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;
|
||||
@ -203,13 +216,14 @@ public class LicensingService : ILicensingService
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!license.VerifyData(user))
|
||||
var claimsPrincipal = GetClaimsPrincipalFromLicense(license);
|
||||
if (!license.VerifyData(user, claimsPrincipal))
|
||||
{
|
||||
await DisablePremiumAsync(user, license, "Invalid data.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!license.VerifySignature(_certificate))
|
||||
if (string.IsNullOrWhiteSpace(license.Token) && !license.VerifySignature(_certificate))
|
||||
{
|
||||
await DisablePremiumAsync(user, license, "Invalid signature.");
|
||||
return false;
|
||||
@ -234,7 +248,21 @@ public class LicensingService : ILicensingService
|
||||
|
||||
public bool VerifyLicense(ILicense license)
|
||||
{
|
||||
return license.VerifySignature(_certificate);
|
||||
if (string.IsNullOrWhiteSpace(license.Token))
|
||||
{
|
||||
return license.VerifySignature(_certificate);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_ = GetClaimsPrincipalFromLicense(license);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogWarning(e, "Invalid token.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] SignLicense(ILicense license)
|
||||
@ -272,4 +300,101 @@ public class LicensingService : ILicensingService
|
||||
using var fs = File.OpenRead(filePath);
|
||||
return await JsonSerializer.DeserializeAsync<OrganizationLicense>(fs);
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
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)
|
||||
{
|
||||
// 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}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> CreateOrganizationTokenAsync(Organization organization, Guid installationId, SubscriptionInfo subscriptionInfo)
|
||||
{
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.SelfHostLicenseRefactor))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var licenseContext = new LicenseContext
|
||||
{
|
||||
InstallationId = installationId,
|
||||
SubscriptionInfo = subscriptionInfo,
|
||||
};
|
||||
|
||||
var claims = await _organizationLicenseClaimsFactory.GenerateClaims(organization, licenseContext);
|
||||
var audience = $"organization:{organization.Id}";
|
||||
|
||||
return GenerateToken(claims, audience);
|
||||
}
|
||||
|
||||
public async Task<string> 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:{user.Id}";
|
||||
|
||||
return GenerateToken(claims, audience);
|
||||
}
|
||||
|
||||
private string GenerateToken(List<Claim> claims, string audience)
|
||||
{
|
||||
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 = DateTime.UtcNow.AddYears(1), // Org expiration is a claim
|
||||
SigningCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.RsaSha256Signature)
|
||||
};
|
||||
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var token = tokenHandler.CreateToken(tokenDescriptor);
|
||||
return tokenHandler.WriteToken(token);
|
||||
}
|
||||
}
|
||||
|
@ -908,7 +908,9 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
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 class UserService : UserManager<User>, IUserService, IDisposable
|
||||
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);
|
||||
}
|
||||
@ -1111,7 +1115,9 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<UserLicense> GenerateLicenseAsync(User user, SubscriptionInfo subscriptionInfo = null,
|
||||
public async Task<UserLicense> GenerateLicenseAsync(
|
||||
User user,
|
||||
SubscriptionInfo subscriptionInfo = null,
|
||||
int? version = null)
|
||||
{
|
||||
if (user == null)
|
||||
@ -1124,8 +1130,13 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
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<bool> CheckPasswordAsync(User user, string password)
|
||||
|
@ -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;
|
||||
@ -53,4 +54,19 @@ public class NoopLicensingService : ILicensingService
|
||||
{
|
||||
return Task.FromResult<OrganizationLicense>(null);
|
||||
}
|
||||
|
||||
public ClaimsPrincipal GetClaimsPrincipalFromLicense(ILicense license)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public Task<string> CreateOrganizationTokenAsync(Organization organization, Guid installationId, SubscriptionInfo subscriptionInfo)
|
||||
{
|
||||
return Task.FromResult<string>(null);
|
||||
}
|
||||
|
||||
public Task<string> CreateUserTokenAsync(User user, SubscriptionInfo subscriptionInfo)
|
||||
{
|
||||
return Task.FromResult<string>(null);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user