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:
@ -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();
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Reference in New Issue
Block a user