1
0
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:
Conner Turnbull
2024-12-05 09:31:14 -05:00
committed by GitHub
parent 0e32dcccad
commit 04cf513d78
23 changed files with 846 additions and 106 deletions

View File

@ -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();

View File

@ -1,10 +1,12 @@
using System.Reflection;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
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;
@ -151,6 +153,7 @@ public class OrganizationLicense : ILicense
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);
/// <summary>
@ -176,6 +179,7 @@ public class OrganizationLicense : ILicense
!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
@ -236,8 +240,65 @@ public class OrganizationLicense : ILicense
}
}
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) || claimsPrincipal is null)
{
return ObsoleteCanUse(globalSettings, licensingService, out exception);
}
var errorMessages = new StringBuilder();
var enabled = claimsPrincipal.GetValue<bool>(nameof(Enabled));
if (!enabled)
{
errorMessages.AppendLine("Your cloud-hosted organization is currently disabled.");
}
var installationId = claimsPrincipal.GetValue<Guid>(nameof(InstallationId));
if (installationId != globalSettings.Installation.Id)
{
errorMessages.AppendLine("The installation ID does not match the current installation.");
}
var selfHost = claimsPrincipal.GetValue<bool>(nameof(SelfHost));
if (!selfHost)
{
errorMessages.AppendLine("The license does not allow for on-premise hosting of organizations.");
}
var licenseType = claimsPrincipal.GetValue<LicenseType>(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;
}
/// <summary>
/// Do not extend this method. It is only here for backwards compatibility with old licenses.
/// Instead, extend the CanUse method using the ClaimsPrincipal.
/// </summary>
/// <param name="globalSettings"></param>
/// <param name="licensingService"></param>
/// <param name="exception"></param>
/// <returns></returns>
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)
@ -291,101 +352,177 @@ public class OrganizationLicense : ILicense
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<DateTime>(nameof(Issued));
var expires = claimsPrincipal.GetValue<DateTime>(nameof(Expires));
var installationId = claimsPrincipal.GetValue<Guid>(nameof(InstallationId));
var licenseKey = claimsPrincipal.GetValue<string>(nameof(LicenseKey));
var enabled = claimsPrincipal.GetValue<bool>(nameof(Enabled));
var planType = claimsPrincipal.GetValue<PlanType>(nameof(PlanType));
var seats = claimsPrincipal.GetValue<int?>(nameof(Seats));
var maxCollections = claimsPrincipal.GetValue<short?>(nameof(MaxCollections));
var useGroups = claimsPrincipal.GetValue<bool>(nameof(UseGroups));
var useDirectory = claimsPrincipal.GetValue<bool>(nameof(UseDirectory));
var useTotp = claimsPrincipal.GetValue<bool>(nameof(UseTotp));
var selfHost = claimsPrincipal.GetValue<bool>(nameof(SelfHost));
var name = claimsPrincipal.GetValue<string>(nameof(Name));
var usersGetPremium = claimsPrincipal.GetValue<bool>(nameof(UsersGetPremium));
var useEvents = claimsPrincipal.GetValue<bool>(nameof(UseEvents));
var use2fa = claimsPrincipal.GetValue<bool>(nameof(Use2fa));
var useApi = claimsPrincipal.GetValue<bool>(nameof(UseApi));
var usePolicies = claimsPrincipal.GetValue<bool>(nameof(UsePolicies));
var useSso = claimsPrincipal.GetValue<bool>(nameof(UseSso));
var useResetPassword = claimsPrincipal.GetValue<bool>(nameof(UseResetPassword));
var useKeyConnector = claimsPrincipal.GetValue<bool>(nameof(UseKeyConnector));
var useScim = claimsPrincipal.GetValue<bool>(nameof(UseScim));
var useCustomPermissions = claimsPrincipal.GetValue<bool>(nameof(UseCustomPermissions));
var useSecretsManager = claimsPrincipal.GetValue<bool>(nameof(UseSecretsManager));
var usePasswordManager = claimsPrincipal.GetValue<bool>(nameof(UsePasswordManager));
var smSeats = claimsPrincipal.GetValue<int?>(nameof(SmSeats));
var smServiceAccounts = claimsPrincipal.GetValue<int?>(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;
}
/// <summary>
/// Do not extend this method. It is only here for backwards compatibility with old licenses.
/// Instead, extend the VerifyData method using the ClaimsPrincipal.
/// </summary>
/// <param name="organization"></param>
/// <param name="globalSettings"></param>
/// <returns></returns>
/// <exception cref="NotSupportedException"></exception>
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;
}
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);
throw new NotSupportedException($"Version {Version} is not supported.");
}
if (valid && Version >= 2)
{
valid = organization.UsersGetPremium == UsersGetPremium;
}
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 >= 3)
{
valid = organization.UseEvents == UseEvents;
}
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;
}
/*Version 12 added ExpirationWithoutDatePeriod, but that property is informational only and is not saved
if (valid && Version >= 11)
{
valid = organization.UseCustomPermissions == UseCustomPermissions;
}
/*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)

View File

@ -1,8 +1,10 @@
using System.Reflection;
using System.Security.Claims;
using System.Security.Cryptography;
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;
@ -70,6 +72,7 @@ public class UserLicense : ILicense
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 +87,7 @@ public class UserLicense : ILicense
!p.Name.Equals(nameof(Signature)) &&
!p.Name.Equals(nameof(SignatureBytes)) &&
!p.Name.Equals(nameof(LicenseType)) &&
!p.Name.Equals(nameof(Token)) &&
(
!forHash ||
(
@ -113,8 +117,47 @@ public class UserLicense : ILicense
}
}
public bool CanUse(User user, out string exception)
public bool CanUse(User user, ClaimsPrincipal claimsPrincipal, out string exception)
{
if (string.IsNullOrWhiteSpace(Token) || claimsPrincipal is null)
{
return ObsoleteCanUse(user, out exception);
}
var errorMessages = new StringBuilder();
if (!user.EmailVerified)
{
errorMessages.AppendLine("The user's email is not verified.");
}
var email = claimsPrincipal.GetValue<string>(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;
}
/// <summary>
/// Do not extend this method. It is only here for backwards compatibility with old licenses.
/// Instead, extend the CanUse method using the ClaimsPrincipal.
/// </summary>
/// <param name="user"></param>
/// <param name="exception"></param>
/// <returns></returns>
/// <exception cref="NotSupportedException"></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)
@ -152,22 +195,46 @@ public class UserLicense : ILicense
return true;
}
public bool VerifyData(User user)
public bool VerifyData(User user, ClaimsPrincipal claimsPrincipal)
{
if (string.IsNullOrWhiteSpace(Token) || claimsPrincipal is null)
{
return ObsoleteVerifyData(user);
}
var licenseKey = claimsPrincipal.GetValue<string>(nameof(LicenseKey));
var premium = claimsPrincipal.GetValue<bool>(nameof(Premium));
var email = claimsPrincipal.GetValue<string>(nameof(Email));
return licenseKey == user.LicenseKey &&
premium == user.Premium &&
email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase);
}
/// <summary>
/// Do not extend this method. It is only here for backwards compatibility with old licenses.
/// Instead, extend the VerifyData method using the ClaimsPrincipal.
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
/// <exception cref="NotSupportedException"></exception>
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;
}
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)