diff --git a/identity-server/src/IdentityServer/Licensing/V2/License.cs b/identity-server/src/IdentityServer/Licensing/V2/License.cs
index 52cb2547e..89774a4ea 100644
--- a/identity-server/src/IdentityServer/Licensing/V2/License.cs
+++ b/identity-server/src/IdentityServer/Licensing/V2/License.cs
@@ -46,13 +46,74 @@ internal class License
{
throw new Exception($"Invalid edition in license: '{edition}'");
}
+
Edition = editionValue;
}
Features = claims.FindAll("feature").Select(f => f.Value).ToArray();
Extras = claims.FindFirst("extras")?.Value ?? string.Empty;
+
+ // IsConfigured needs to be set prior to checking for clients and issuers claims or the Redistribution check will not return an appropriate value
IsConfigured = true;
+
+ if (!claims.HasClaim("feature", "unlimited_clients"))
+ {
+ // default values
+ if (Redistribution)
+ {
+ // default for all ISV editions
+ ClientLimit = 5;
+ }
+ else
+ {
+ // defaults limits for non-ISV editions
+ ClientLimit = Edition switch
+ {
+ LicenseEdition.Business => 15,
+ LicenseEdition.Starter => 5,
+ _ => ClientLimit
+ };
+ }
+
+ if (int.TryParse(claims.FindFirst("client_limit")?.Value, out var clientLimit))
+ {
+ // explicit, so use that value
+ ClientLimit = clientLimit;
+ }
+
+ if (!Redistribution)
+ {
+ // these for the non-ISV editions that always have unlimited, regardless of explicit value
+ ClientLimit = Edition switch
+ {
+ LicenseEdition.Enterprise or LicenseEdition.Community =>
+ // unlimited
+ null,
+ _ => ClientLimit
+ };
+ }
+ }
+
+ if (!claims.HasClaim("feature", "unlimited_issuers"))
+ {
+ // default
+ IssuerLimit = 1;
+
+ if (int.TryParse(claims.FindFirst("issuer_limit")?.Value, out var issuerLimit))
+ {
+ IssuerLimit = issuerLimit;
+ }
+
+ // these for the editions that always have unlimited, regardless of explicit value
+ IssuerLimit = Edition switch
+ {
+ LicenseEdition.Enterprise or LicenseEdition.Community =>
+ // unlimited
+ null,
+ _ => IssuerLimit
+ };
+ }
}
///
@@ -64,6 +125,7 @@ internal class License
/// The company name
///
public string? CompanyName { get; init; }
+
///
/// The company contact info
///
@@ -82,7 +144,17 @@ internal class License
///
/// True if redistribution is enabled for this license, and false otherwise.
///
- public bool Redistribution => IsEnabled(LicenseFeature.Redistribution) || IsEnabled(LicenseFeature.ISV);
+ public bool Redistribution => IsConfigured && (IsEnabled(LicenseFeature.Redistribution) || IsEnabled(LicenseFeature.ISV));
+
+ ///
+ /// The number of clients this license allows, or null if the license allows unlimited clients.
+ ///
+ public int? ClientLimit { get; init; }
+
+ ///
+ /// The number of issuers this license allows, or null if the license allows unlimited issuers.
+ ///
+ public int? IssuerLimit { get; init; }
///
/// The license features
diff --git a/identity-server/test/IdentityServer.UnitTests/Licensing/v2/LicenseAccessorTests.cs b/identity-server/test/IdentityServer.UnitTests/Licensing/v2/LicenseAccessorTests.cs
index 8f25f64e3..187761e16 100644
--- a/identity-server/test/IdentityServer.UnitTests/Licensing/v2/LicenseAccessorTests.cs
+++ b/identity-server/test/IdentityServer.UnitTests/Licensing/v2/LicenseAccessorTests.cs
@@ -22,7 +22,7 @@ public class LicenseAccessorTests
[Theory]
[MemberData(nameof(LicenseTestCases))]
- internal void license_set_in_options_is_parsed_correctly(int serialNumber, LicenseEdition edition, bool isRedistribution, string contact, bool addDynamicProviders, bool addKeyManagement, string key)
+ internal void license_set_in_options_is_parsed_correctly(int serialNumber, LicenseEdition edition, bool isRedistribution, string contact, bool addDynamicProviders, bool addKeyManagement, int? allowedClients, int? allowedIssuers, string key)
{
_options.LicenseKey = key;
@@ -36,6 +36,8 @@ public class LicenseAccessorTests
l.SerialNumber.ShouldBe(serialNumber);
l.Expiration!.Value.Date.ShouldBe(new DateTime(2024, 11, 15));
l.Redistribution.ShouldBe(isRedistribution);
+ l.ClientLimit.ShouldBe(allowedClients);
+ l.IssuerLimit.ShouldBe(allowedIssuers);
var enterpriseFeaturesEnabled = edition == LicenseEdition.Enterprise || edition == LicenseEdition.Community;
var businessFeaturesEnabled = enterpriseFeaturesEnabled || edition == LicenseEdition.Business;
@@ -51,26 +53,54 @@ public class LicenseAccessorTests
_licenseAccessor.Current.IsEnabled(LicenseFeature.ServerSideSessions).ShouldBe(businessFeaturesEnabled);
}
+ [Fact]
+ public void license_not_present_initializes_correctly()
+ {
+ _options.LicenseKey = null;
+
+ var l = _licenseAccessor.Current;
+
+ l.IsConfigured.ShouldBeFalse();
+ l.Edition.ShouldBeNull();
+ l.Extras.ShouldBeNull();
+ l.CompanyName.ShouldBeNull();
+ l.ContactInfo.ShouldBeNull();
+ l.SerialNumber.ShouldBeNull();
+ l.Expiration.ShouldBeNull();
+ l.Redistribution.ShouldBeFalse();
+ l.ClientLimit.ShouldBeNull();
+ l.IssuerLimit.ShouldBeNull();
+
+ _licenseAccessor.Current.IsEnabled(LicenseFeature.DynamicProviders).ShouldBeTrue();
+ _licenseAccessor.Current.IsEnabled(LicenseFeature.ResourceIsolation).ShouldBeTrue();
+ _licenseAccessor.Current.IsEnabled(LicenseFeature.DPoP).ShouldBeTrue();
+ _licenseAccessor.Current.IsEnabled(LicenseFeature.CIBA).ShouldBeTrue();
+
+ _licenseAccessor.Current.IsEnabled(LicenseFeature.KeyManagement).ShouldBeTrue();
+ _licenseAccessor.Current.IsEnabled(LicenseFeature.PAR).ShouldBeTrue();
+ _licenseAccessor.Current.IsEnabled(LicenseFeature.DCR).ShouldBeTrue();
+ _licenseAccessor.Current.IsEnabled(LicenseFeature.ServerSideSessions).ShouldBeTrue();
+ }
public static IEnumerable