Merge pull request #2279 from DuendeSoftware/pg/bff-trial-mode

Introduce BFF Trial Mode
This commit is contained in:
Pieter Germishuys 2025-11-28 09:24:38 +01:00 committed by GitHub
commit 7703ed610b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 229 additions and 270 deletions

View file

@ -97,6 +97,11 @@ public static class BffBuilderExtensions
builder.Services
.AddSingleton<IPostConfigureOptions<OpenIdConnectOptions>, PostConfigureOidcOptionsForSilentLogin>();
builder.Services.AddSingleton<TrialModeAuthenticatedSessionTracker>();
builder.Services
.AddSingleton<IPostConfigureOptions<CookieAuthenticationOptions>,
PostConfigureApplicationCookieTrialModeCheck>();
AddBffMetrics(builder);
// wrap ASP.NET Core

View file

@ -44,7 +44,10 @@ internal class LicenseAccessor(GetLicenseKey getLicenseKey, ILogger<LicenseAcces
}
var licenseClaims = ValidateKey(key);
return new License(new ClaimsPrincipal(new ClaimsIdentity(licenseClaims)));
return new License(new ClaimsPrincipal(new ClaimsIdentity(licenseClaims)))
{
IsConfigured = licenseClaims.Length != 0
};
}
}

View file

@ -14,6 +14,7 @@ internal class LicenseValidator(ILogger<LicenseValidator> logger, License licens
}
private bool? _licenseCheckResult;
internal const int MaximumAllowedSessionsInTrialMode = 5;
public bool CheckLicense()
{
@ -34,13 +35,19 @@ internal class LicenseValidator(ILogger<LicenseValidator> logger, License licens
return false;
}
//An expired license is still considered a valid license.
if (license.Expiration <= timeProvider.GetUtcNow())
{
logger.LicenseHasExpired(LogLevel.Error, license.Expiration, license.ContactInfo ?? "",
license.CompanyName ?? "");
return false;
logger.LicenseHasExpired(LogLevel.Warning, license.Expiration, license.ContactInfo,
license.CompanyName);
}
logger.LicenseDetails(
LogLevel.Debug,
license.Expiration,
license.ContactInfo,
license.CompanyName);
return true;
}
}

View file

@ -9,82 +9,48 @@
// {
// [LoggerMessage(
// Message = """
// Duende BFF Security Framework License information:
// - Edition: {Edition}
// - Expiration: {ExpirationDate}
// - LicenseContact: {LicenseContact}
// - LicenseCompany: {licenseCompany}
// - Number of frontends licensed: {NumberOfFrontends}
// """)]
// public static partial void LicenseDetails(this ILogger logger, LogLevel level, string? edition, DateTimeOffset? expirationDate, string licenseContact, string licenseCompany, string? numberOfFrontends);
// Duende Software License information:
// - Expiration: {ExpirationDate}
// - LicenseContact: {LicenseContact}
// - LicenseCompany: {licenseCompany}
// """)]
// public static partial void LicenseDetails(this ILogger logger, LogLevel level,
// DateTimeOffset? expirationDate, string licenseContact, string licenseCompany);
//
// [LoggerMessage(
// Message = $$"""
// Your license for the Duende Software has expired on {ExpirationDate}.
// Please contact {licenseContact} from {licenseCompany} to obtain a valid license for Duende software,
// or start a conversation with us: https://duende.link/l/bff/contact
// Message = """
// Your license for the Duende software has expired on {ExpirationDate}.
// Please contact {licenseContact} from {licenseCompany} to obtain a valid license for Duende software,
// or start a conversation with us: https://duende.link/l/bff/contact
//
// See https://duende.link/l/bff/expired for more information.
// """)]
// public static partial void LicenseHasExpired(this ILogger logger, LogLevel level, DateTimeOffset? expirationDate, string licenseContact, string licenseCompany);
// See https://duende.link/l/bff/expired for more information.
// """)]
// public static partial void LicenseHasExpired(this ILogger logger, LogLevel level, DateTimeOffset? expirationDate,
// string licenseContact, string licenseCompany);
//
//
// [LoggerMessage(
// message: """
// You do not have a valid license key for the Duende software.
// This is allowed for development and testing scenarios.
// If you are running in production you are required to have a licensed version.
// Please start a conversation with us: https://duende.link/l/contact"
// """)]
// Message = """
// You do not have a valid license key for the Duende software.
// BFF will run in trial mode. This is allowed for development and testing scenarios.
//
// If you are running in production you are required to have a licensed version.
// Please start a conversation with us: https://duende.link/l/bff/contact"
// """)]
// public static partial void NoValidLicense(this ILogger logger, LogLevel logLevel);
//
// [LoggerMessage(
// message: """
// Your license key does not include the BFF feature.
// BFF will run in trial mode. It will limit the number of active sessions to 5.
// Please contact {LicenseContact} from {LicenseCompany} to obtain a valid license for the Duende software,
// or start a conversation with us: https://duende.link/l/bff/contact
// Message = """
// BFF is running in trial mode. The maximum number of allowed authenticated sessions ({MaximumAllowedSessionsInTrialMode}) has been exceeded.
//
// See https://duende.link/l/bff/trial for more information.
// """)]
// public static partial void NotLicensedForBff(this ILogger logger, LogLevel logLevel, string licenseContact, string licenseCompany);
// See https://duende.link/l/bff/trial for more information.
// """)]
// public static partial void TrialModeWarning(this ILogger logger, LogLevel logLevel,
// int maximumAllowedSessionsInTrialMode);
//
// [LoggerMessage(
// message: "Error validating the license key." +
// "If you are running in production you are required to have a licensed version. " +
// "Please start a conversation with us: https://duende.link/l/bff/contact")]
// Message = "Error validating the license key." +
// "If you are running in production you are required to have a licensed version. " +
// "Please start a conversation with us: https://duende.link/l/bff/contact")]
// public static partial void ErrorValidatingLicenseKey(this ILogger logger, LogLevel logLevel, Exception ex);
//
// [LoggerMessage(
// message: """
// Frontend #{FrontendsUsed} with name {FrontendName} was added. The license allows for unlimited frontends.
// """)]
// public static partial void UnlimitedFrontends(this ILogger logger, LogLevel logLevel, string frontendName,
// int frontendsUsed);
// [LoggerMessage(
// message: """
// Frontend {FrontendName} was added. Currently using {frontendsUsed} of {frontendLimit} in the BFF License.
// """)]
// public static partial void FrontendAdded(this ILogger logger, LogLevel logLevel, string frontendName,
// int frontendsUsed, int frontendLimit);
//
// [LoggerMessage(
// message: """
// Frontend {FrontendName} was added. This exceeds the maximum number of frontends allowed by your license.
// Currently using {frontendsUsed} of {frontendLimit} in the BFF License.
//
// See https://duende.link/l/bff/threshold for more information.
// """)]
// public static partial void FrontendLimitExceeded(this ILogger logger, LogLevel logLevel, string frontendName,
// int frontendsUsed, int frontendLimit);
//
// [LoggerMessage(
// message: """
// Frontend {FrontendName} was added. However, your current license does not support multiple frontends.
// If you are running in production you are required to have a license for each frontend.
// Please start a conversation with us: https://duende.link/l/bff/contact
//
// See https://duende.link/l/bff/threshold for more information.
// """)]
// public static partial void NotLicensedForMultiFrontend(this ILogger logger, LogLevel logLevel, string frontendName);
// }

View file

@ -0,0 +1,21 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using System.Collections.Concurrent;
namespace Duende.Bff.Licensing;
internal class TrialModeAuthenticatedSessionTracker
{
private readonly ConcurrentDictionary<string, byte> _authenticatedSessions = new();
public int UniqueAuthenticatedSessions => _authenticatedSessions.Count;
public void RecordAuthenticatedSession(string subjectId)
{
if (_authenticatedSessions.Count <= LicenseValidator.MaximumAllowedSessionsInTrialMode)
{
_authenticatedSessions.TryAdd(subjectId, 0);
}
}
}

View file

@ -90,10 +90,10 @@ namespace Duende.Bff.Licensing
internal static class LicensingLogMessages
{
/// <summary>
/// Logs "Duende BFF Security Framework License information:\r\n - Edition: {Edition}\r\n - Expiration: {ExpirationDate}\r\n - LicenseContact: {LicenseContact}\r\n - LicenseCompany: {licenseCompany}\r\n - Number of frontends licensed: {NumberOfFrontends}".
/// Logs "Duende Software License information:\r\n - Expiration: {ExpirationDate}\r\n - LicenseContact: {LicenseContact}\r\n - LicenseCompany: {licenseCompany}".
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Gen.Logging", "10.0.0.0")]
public static void LicenseDetails(this global::Microsoft.Extensions.Logging.ILogger logger, global::Microsoft.Extensions.Logging.LogLevel level, string? edition, global::System.DateTimeOffset? expirationDate, string licenseContact, string licenseCompany, string? numberOfFrontends)
public static void LicenseDetails(this global::Microsoft.Extensions.Logging.ILogger logger, global::Microsoft.Extensions.Logging.LogLevel level, global::System.DateTimeOffset? expirationDate, string licenseContact, string licenseCompany)
{
if (!logger.IsEnabled(level))
{
@ -102,13 +102,11 @@ namespace Duende.Bff.Licensing
var state = global::Microsoft.Extensions.Logging.LoggerMessageHelper.ThreadLocalState;
_ = state.ReserveTagSpace(6);
state.TagArray[5] = new("{OriginalFormat}", "Duende BFF Security Framework License information:\r\n - Edition: {Edition}\r\n - Expiration: {ExpirationDate}\r\n - LicenseContact: {LicenseContact}\r\n - LicenseCompany: {licenseCompany}\r\n - Number of frontends licensed: {NumberOfFrontends}");
state.TagArray[4] = new("Edition", edition);
state.TagArray[3] = new("ExpirationDate", expirationDate);
state.TagArray[2] = new("LicenseContact", licenseContact);
state.TagArray[1] = new("licenseCompany", licenseCompany);
state.TagArray[0] = new("NumberOfFrontends", numberOfFrontends);
_ = state.ReserveTagSpace(4);
state.TagArray[3] = new("{OriginalFormat}", "Duende Software License information:\r\n - Expiration: {ExpirationDate}\r\n - LicenseContact: {LicenseContact}\r\n - LicenseCompany: {licenseCompany}");
state.TagArray[2] = new("ExpirationDate", expirationDate);
state.TagArray[1] = new("LicenseContact", licenseContact);
state.TagArray[0] = new("licenseCompany", licenseCompany);
logger.Log(
level,
@ -117,15 +115,13 @@ namespace Duende.Bff.Licensing
null,
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Gen.Logging", "10.0.0.0")] static string (s, _) =>
{
var edition = s.TagArray[4].Value ?? "(null)";
var expirationDate = s.TagArray[3].Value ?? "(null)";
var licenseContact = s.TagArray[2].Value ?? "(null)";
var licenseCompany = s.TagArray[1].Value ?? "(null)";
var numberOfFrontends = s.TagArray[0].Value ?? "(null)";
var expirationDate = s.TagArray[2].Value ?? "(null)";
var licenseContact = s.TagArray[1].Value ?? "(null)";
var licenseCompany = s.TagArray[0].Value ?? "(null)";
#if NET
return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"Duende BFF Security Framework License information:\r\n - Edition: {edition}\r\n - Expiration: {expirationDate}\r\n - LicenseContact: {licenseContact}\r\n - LicenseCompany: {licenseCompany}\r\n - Number of frontends licensed: {numberOfFrontends}");
return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"Duende Software License information:\r\n - Expiration: {expirationDate}\r\n - LicenseContact: {licenseContact}\r\n - LicenseCompany: {licenseCompany}");
#else
return global::System.FormattableString.Invariant($"Duende BFF Security Framework License information:\r\n - Edition: {edition}\r\n - Expiration: {expirationDate}\r\n - LicenseContact: {licenseContact}\r\n - LicenseCompany: {licenseCompany}\r\n - Number of frontends licensed: {numberOfFrontends}");
return global::System.FormattableString.Invariant($"Duende Software License information:\r\n - Expiration: {expirationDate}\r\n - LicenseContact: {licenseContact}\r\n - LicenseCompany: {licenseCompany}");
#endif
});
@ -133,7 +129,7 @@ namespace Duende.Bff.Licensing
}
/// <summary>
/// Logs "Your license for the Duende Software has expired on {ExpirationDate}.\r\nPlease contact {licenseContact} from {licenseCompany} to obtain a valid license for Duende software,\r\nor start a conversation with us: https://duende.link/l/bff/contact\r\n\r\nSee https://duende.link/l/bff/expired for more information.".
/// Logs "Your license for the Duende software has expired on {ExpirationDate}.\r\nPlease contact {licenseContact} from {licenseCompany} to obtain a valid license for Duende software,\r\nor start a conversation with us: https://duende.link/l/bff/contact\r\n\r\nSee https://duende.link/l/bff/expired for more information.".
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Gen.Logging", "10.0.0.0")]
public static void LicenseHasExpired(this global::Microsoft.Extensions.Logging.ILogger logger, global::Microsoft.Extensions.Logging.LogLevel level, global::System.DateTimeOffset? expirationDate, string licenseContact, string licenseCompany)
@ -146,7 +142,7 @@ namespace Duende.Bff.Licensing
var state = global::Microsoft.Extensions.Logging.LoggerMessageHelper.ThreadLocalState;
_ = state.ReserveTagSpace(4);
state.TagArray[3] = new("{OriginalFormat}", "Your license for the Duende Software has expired on {ExpirationDate}.\r\nPlease contact {licenseContact} from {licenseCompany} to obtain a valid license for Duende software,\r\nor start a conversation with us: https://duende.link/l/bff/contact\r\n\r\nSee https://duende.link/l/bff/expired for more information.");
state.TagArray[3] = new("{OriginalFormat}", "Your license for the Duende software has expired on {ExpirationDate}.\r\nPlease contact {licenseContact} from {licenseCompany} to obtain a valid license for Duende software,\r\nor start a conversation with us: https://duende.link/l/bff/contact\r\n\r\nSee https://duende.link/l/bff/expired for more information.");
state.TagArray[2] = new("ExpirationDate", expirationDate);
state.TagArray[1] = new("licenseContact", licenseContact);
state.TagArray[0] = new("licenseCompany", licenseCompany);
@ -162,9 +158,9 @@ namespace Duende.Bff.Licensing
var licenseContact = s.TagArray[1].Value ?? "(null)";
var licenseCompany = s.TagArray[0].Value ?? "(null)";
#if NET
return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"Your license for the Duende Software has expired on {expirationDate}.\r\nPlease contact {licenseContact} from {licenseCompany} to obtain a valid license for Duende software,\r\nor start a conversation with us: https://duende.link/l/bff/contact\r\n\r\nSee https://duende.link/l/bff/expired for more information.");
return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"Your license for the Duende software has expired on {expirationDate}.\r\nPlease contact {licenseContact} from {licenseCompany} to obtain a valid license for Duende software,\r\nor start a conversation with us: https://duende.link/l/bff/contact\r\n\r\nSee https://duende.link/l/bff/expired for more information.");
#else
return global::System.FormattableString.Invariant($"Your license for the Duende Software has expired on {expirationDate}.\r\nPlease contact {licenseContact} from {licenseCompany} to obtain a valid license for Duende software,\r\nor start a conversation with us: https://duende.link/l/bff/contact\r\n\r\nSee https://duende.link/l/bff/expired for more information.");
return global::System.FormattableString.Invariant($"Your license for the Duende software has expired on {expirationDate}.\r\nPlease contact {licenseContact} from {licenseCompany} to obtain a valid license for Duende software,\r\nor start a conversation with us: https://duende.link/l/bff/contact\r\n\r\nSee https://duende.link/l/bff/expired for more information.");
#endif
});
@ -172,7 +168,7 @@ namespace Duende.Bff.Licensing
}
/// <summary>
/// Logs "You do not have a valid license key for the Duende software.\r\nThis is allowed for development and testing scenarios.\r\nIf you are running in production you are required to have a licensed version.\r\nPlease start a conversation with us: https://duende.link/l/contact"".
/// Logs "You do not have a valid license key for the Duende software.\r\nBFF will run in trial mode. This is allowed for development and testing scenarios.\r\n\r\nIf you are running in production you are required to have a licensed version.\r\nPlease start a conversation with us: https://duende.link/l/bff/contact"".
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Gen.Logging", "10.0.0.0")]
public static void NoValidLicense(this global::Microsoft.Extensions.Logging.ILogger logger, global::Microsoft.Extensions.Logging.LogLevel logLevel)
@ -185,7 +181,7 @@ namespace Duende.Bff.Licensing
var state = global::Microsoft.Extensions.Logging.LoggerMessageHelper.ThreadLocalState;
_ = state.ReserveTagSpace(1);
state.TagArray[0] = new("{OriginalFormat}", "You do not have a valid license key for the Duende software.\r\nThis is allowed for development and testing scenarios.\r\nIf you are running in production you are required to have a licensed version.\r\nPlease start a conversation with us: https://duende.link/l/contact\"");
state.TagArray[0] = new("{OriginalFormat}", "You do not have a valid license key for the Duende software.\r\nBFF will run in trial mode. This is allowed for development and testing scenarios.\r\n\r\nIf you are running in production you are required to have a licensed version.\r\nPlease start a conversation with us: https://duende.link/l/bff/contact\"");
logger.Log(
logLevel,
@ -194,17 +190,17 @@ namespace Duende.Bff.Licensing
null,
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Gen.Logging", "10.0.0.0")] static string (s, _) =>
{
return "You do not have a valid license key for the Duende software.\r\nThis is allowed for development and testing scenarios.\r\nIf you are running in production you are required to have a licensed version.\r\nPlease start a conversation with us: https://duende.link/l/contact\"";
return "You do not have a valid license key for the Duende software.\r\nBFF will run in trial mode. This is allowed for development and testing scenarios.\r\n\r\nIf you are running in production you are required to have a licensed version.\r\nPlease start a conversation with us: https://duende.link/l/bff/contact\"";
});
state.Clear();
}
/// <summary>
/// Logs "Your license key does not include the BFF feature.\r\nBFF will run in trial mode. It will limit the number of active sessions to 5.\r\nPlease contact {LicenseContact} from {LicenseCompany} to obtain a valid license for the Duende software,\r\nor start a conversation with us: https://duende.link/l/bff/contact\r\n\r\nSee https://duende.link/l/bff/trial for more information.".
/// Logs "BFF is running in trial mode. The maximum number of allowed authenticated sessions ({MaximumAllowedSessionsInTrialMode}) has been exceeded.\r\n\r\nSee https://duende.link/l/bff/trial for more information.".
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Gen.Logging", "10.0.0.0")]
public static void NotLicensedForBff(this global::Microsoft.Extensions.Logging.ILogger logger, global::Microsoft.Extensions.Logging.LogLevel logLevel, string licenseContact, string licenseCompany)
public static void TrialModeWarning(this global::Microsoft.Extensions.Logging.ILogger logger, global::Microsoft.Extensions.Logging.LogLevel logLevel, int maximumAllowedSessionsInTrialMode)
{
if (!logger.IsEnabled(logLevel))
{
@ -213,24 +209,22 @@ namespace Duende.Bff.Licensing
var state = global::Microsoft.Extensions.Logging.LoggerMessageHelper.ThreadLocalState;
_ = state.ReserveTagSpace(3);
state.TagArray[2] = new("{OriginalFormat}", "Your license key does not include the BFF feature.\r\nBFF will run in trial mode. It will limit the number of active sessions to 5.\r\nPlease contact {LicenseContact} from {LicenseCompany} to obtain a valid license for the Duende software,\r\nor start a conversation with us: https://duende.link/l/bff/contact\r\n\r\nSee https://duende.link/l/bff/trial for more information.");
state.TagArray[1] = new("LicenseContact", licenseContact);
state.TagArray[0] = new("LicenseCompany", licenseCompany);
_ = state.ReserveTagSpace(2);
state.TagArray[1] = new("{OriginalFormat}", "BFF is running in trial mode. The maximum number of allowed authenticated sessions ({MaximumAllowedSessionsInTrialMode}) has been exceeded.\r\n\r\nSee https://duende.link/l/bff/trial for more information.");
state.TagArray[0] = new("MaximumAllowedSessionsInTrialMode", maximumAllowedSessionsInTrialMode);
logger.Log(
logLevel,
new(792057768, nameof(NotLicensedForBff)),
new(875645872, nameof(TrialModeWarning)),
state,
null,
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Gen.Logging", "10.0.0.0")] static string (s, _) =>
{
var licenseContact = s.TagArray[1].Value ?? "(null)";
var licenseCompany = s.TagArray[0].Value ?? "(null)";
var maximumAllowedSessionsInTrialMode = s.TagArray[0].Value;
#if NET
return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"Your license key does not include the BFF feature.\r\nBFF will run in trial mode. It will limit the number of active sessions to 5.\r\nPlease contact {licenseContact} from {licenseCompany} to obtain a valid license for the Duende software,\r\nor start a conversation with us: https://duende.link/l/bff/contact\r\n\r\nSee https://duende.link/l/bff/trial for more information.");
return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"BFF is running in trial mode. The maximum number of allowed authenticated sessions ({maximumAllowedSessionsInTrialMode}) has been exceeded.\r\n\r\nSee https://duende.link/l/bff/trial for more information.");
#else
return global::System.FormattableString.Invariant($"Your license key does not include the BFF feature.\r\nBFF will run in trial mode. It will limit the number of active sessions to 5.\r\nPlease contact {licenseContact} from {licenseCompany} to obtain a valid license for the Duende software,\r\nor start a conversation with us: https://duende.link/l/bff/contact\r\n\r\nSee https://duende.link/l/bff/trial for more information.");
return global::System.FormattableString.Invariant($"BFF is running in trial mode. The maximum number of allowed authenticated sessions ({maximumAllowedSessionsInTrialMode}) has been exceeded.\r\n\r\nSee https://duende.link/l/bff/trial for more information.");
#endif
});
@ -265,156 +259,6 @@ namespace Duende.Bff.Licensing
state.Clear();
}
/// <summary>
/// Logs "Frontend #{FrontendsUsed} with name {FrontendName} was added. The license allows for unlimited frontends.".
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Gen.Logging", "10.0.0.0")]
public static void UnlimitedFrontends(this global::Microsoft.Extensions.Logging.ILogger logger, global::Microsoft.Extensions.Logging.LogLevel logLevel, string frontendName, int frontendsUsed)
{
if (!logger.IsEnabled(logLevel))
{
return;
}
var state = global::Microsoft.Extensions.Logging.LoggerMessageHelper.ThreadLocalState;
_ = state.ReserveTagSpace(3);
state.TagArray[2] = new("{OriginalFormat}", "Frontend #{FrontendsUsed} with name {FrontendName} was added. The license allows for unlimited frontends.");
state.TagArray[1] = new("FrontendName", frontendName);
state.TagArray[0] = new("FrontendsUsed", frontendsUsed);
logger.Log(
logLevel,
new(1958275515, nameof(UnlimitedFrontends)),
state,
null,
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Gen.Logging", "10.0.0.0")] static string (s, _) =>
{
var frontendName = s.TagArray[1].Value ?? "(null)";
var frontendsUsed = s.TagArray[0].Value;
#if NET
return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"Frontend #{frontendsUsed} with name {frontendName} was added. The license allows for unlimited frontends.");
#else
return global::System.FormattableString.Invariant($"Frontend #{frontendsUsed} with name {frontendName} was added. The license allows for unlimited frontends.");
#endif
});
state.Clear();
}
/// <summary>
/// Logs "Frontend {FrontendName} was added. Currently using {frontendsUsed} of {frontendLimit} in the BFF License.".
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Gen.Logging", "10.0.0.0")]
public static void FrontendAdded(this global::Microsoft.Extensions.Logging.ILogger logger, global::Microsoft.Extensions.Logging.LogLevel logLevel, string frontendName, int frontendsUsed, int frontendLimit)
{
if (!logger.IsEnabled(logLevel))
{
return;
}
var state = global::Microsoft.Extensions.Logging.LoggerMessageHelper.ThreadLocalState;
_ = state.ReserveTagSpace(4);
state.TagArray[3] = new("{OriginalFormat}", "Frontend {FrontendName} was added. Currently using {frontendsUsed} of {frontendLimit} in the BFF License.");
state.TagArray[2] = new("FrontendName", frontendName);
state.TagArray[1] = new("frontendsUsed", frontendsUsed);
state.TagArray[0] = new("frontendLimit", frontendLimit);
logger.Log(
logLevel,
new(231888333, nameof(FrontendAdded)),
state,
null,
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Gen.Logging", "10.0.0.0")] static string (s, _) =>
{
var frontendName = s.TagArray[2].Value ?? "(null)";
var frontendsUsed = s.TagArray[1].Value;
var frontendLimit = s.TagArray[0].Value;
#if NET
return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"Frontend {frontendName} was added. Currently using {frontendsUsed} of {frontendLimit} in the BFF License.");
#else
return global::System.FormattableString.Invariant($"Frontend {frontendName} was added. Currently using {frontendsUsed} of {frontendLimit} in the BFF License.");
#endif
});
state.Clear();
}
/// <summary>
/// Logs "Frontend {FrontendName} was added. This exceeds the maximum number of frontends allowed by your license.\r\nCurrently using {frontendsUsed} of {frontendLimit} in the BFF License.\r\n\r\nSee https://duende.link/l/bff/threshold for more information.".
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Gen.Logging", "10.0.0.0")]
public static void FrontendLimitExceeded(this global::Microsoft.Extensions.Logging.ILogger logger, global::Microsoft.Extensions.Logging.LogLevel logLevel, string frontendName, int frontendsUsed, int frontendLimit)
{
if (!logger.IsEnabled(logLevel))
{
return;
}
var state = global::Microsoft.Extensions.Logging.LoggerMessageHelper.ThreadLocalState;
_ = state.ReserveTagSpace(4);
state.TagArray[3] = new("{OriginalFormat}", "Frontend {FrontendName} was added. This exceeds the maximum number of frontends allowed by your license.\r\nCurrently using {frontendsUsed} of {frontendLimit} in the BFF License.\r\n\r\nSee https://duende.link/l/bff/threshold for more information.");
state.TagArray[2] = new("FrontendName", frontendName);
state.TagArray[1] = new("frontendsUsed", frontendsUsed);
state.TagArray[0] = new("frontendLimit", frontendLimit);
logger.Log(
logLevel,
new(438656937, nameof(FrontendLimitExceeded)),
state,
null,
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Gen.Logging", "10.0.0.0")] static string (s, _) =>
{
var frontendName = s.TagArray[2].Value ?? "(null)";
var frontendsUsed = s.TagArray[1].Value;
var frontendLimit = s.TagArray[0].Value;
#if NET
return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"Frontend {frontendName} was added. This exceeds the maximum number of frontends allowed by your license.\r\nCurrently using {frontendsUsed} of {frontendLimit} in the BFF License.\r\n\r\nSee https://duende.link/l/bff/threshold for more information.");
#else
return global::System.FormattableString.Invariant($"Frontend {frontendName} was added. This exceeds the maximum number of frontends allowed by your license.\r\nCurrently using {frontendsUsed} of {frontendLimit} in the BFF License.\r\n\r\nSee https://duende.link/l/bff/threshold for more information.");
#endif
});
state.Clear();
}
/// <summary>
/// Logs "Frontend {FrontendName} was added. However, your current license does not support multiple frontends.\r\nIf you are running in production you are required to have a license for each frontend.\r\nPlease start a conversation with us: https://duende.link/l/bff/contact\r\n\r\nSee https://duende.link/l/bff/threshold for more information.".
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Gen.Logging", "10.0.0.0")]
public static void NotLicensedForMultiFrontend(this global::Microsoft.Extensions.Logging.ILogger logger, global::Microsoft.Extensions.Logging.LogLevel logLevel, string frontendName)
{
if (!logger.IsEnabled(logLevel))
{
return;
}
var state = global::Microsoft.Extensions.Logging.LoggerMessageHelper.ThreadLocalState;
_ = state.ReserveTagSpace(2);
state.TagArray[1] = new("{OriginalFormat}", "Frontend {FrontendName} was added. However, your current license does not support multiple frontends.\r\nIf you are running in production you are required to have a license for each frontend.\r\nPlease start a conversation with us: https://duende.link/l/bff/contact\r\n\r\nSee https://duende.link/l/bff/threshold for more information.");
state.TagArray[0] = new("FrontendName", frontendName);
logger.Log(
logLevel,
new(289456605, nameof(NotLicensedForMultiFrontend)),
state,
null,
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Gen.Logging", "10.0.0.0")] static string (s, _) =>
{
var frontendName = s.TagArray[0].Value ?? "(null)";
#if NET
return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"Frontend {frontendName} was added. However, your current license does not support multiple frontends.\r\nIf you are running in production you are required to have a license for each frontend.\r\nPlease start a conversation with us: https://duende.link/l/bff/contact\r\n\r\nSee https://duende.link/l/bff/threshold for more information.");
#else
return global::System.FormattableString.Invariant($"Frontend {frontendName} was added. However, your current license does not support multiple frontends.\r\nIf you are running in production you are required to have a license for each frontend.\r\nPlease start a conversation with us: https://duende.link/l/bff/contact\r\n\r\nSee https://duende.link/l/bff/threshold for more information.");
#endif
});
state.Clear();
}
}
}
@ -3683,7 +3527,6 @@ namespace Duende.Bff.Otel
state.Clear();
}
public static string Sanitize(this string toSanitize) => toSanitize.ReplaceLineEndings(string.Empty);
public static string Sanitize(this PathString toSanitize) => toSanitize.ToString().ReplaceLineEndings(string.Empty);

View file

@ -445,8 +445,6 @@
// message: $"Failed to add frontend change to {{{OTelParameters.Frontend}}} to queue")]
// public static partial void FailedToAddFrontendToQueue(this ILogger logger, LogLevel logLevel, BffFrontendName frontend);
//
//
//
// public static string Sanitize(this string toSanitize) => toSanitize.ReplaceLineEndings(string.Empty);
//
// public static string Sanitize(this PathString toSanitize) => toSanitize.ToString().ReplaceLineEndings(string.Empty);

View file

@ -0,0 +1,58 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Duende.Bff.AccessTokenManagement;
using Duende.Bff.Internal;
using Duende.Bff.Licensing;
using Duende.IdentityModel;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Duende.Bff.SessionManagement.Configuration;
/// <summary>
/// Cookie configuration to check license validity when a user authenticates.
/// </summary>
internal class PostConfigureApplicationCookieTrialModeCheck(
ActiveCookieAuthenticationScheme activeCookieScheme,
LicenseValidator licenseValidator,
TrialModeAuthenticatedSessionTracker authenticatedSessionTracker,
ILogger<PostConfigureApplicationCookieTrialModeCheck> logger)
: IPostConfigureOptions<CookieAuthenticationOptions>
{
/// <inheritdoc />
public void PostConfigure(string? name, CookieAuthenticationOptions options)
{
if (!activeCookieScheme.ShouldConfigureScheme(Scheme.ParseOrDefault(name)))
{
return;
}
if (!licenseValidator.CheckLicense())
{
options.Events.OnSigningIn = CreateCallback(options.Events.OnSigningIn);
}
}
private Func<CookieSigningInContext, Task> CreateCallback(Func<CookieSigningInContext, Task> inner)
{
async Task Callback(CookieSigningInContext ctx)
{
var subjectId = ctx.Principal?.FindFirst(JwtClaimTypes.Subject)?.Value
?? "unknown";
authenticatedSessionTracker.RecordAuthenticatedSession(subjectId);
if (authenticatedSessionTracker.UniqueAuthenticatedSessions >
LicenseValidator.MaximumAllowedSessionsInTrialMode)
{
logger.TrialModeWarning(LogLevel.Error, LicenseValidator.MaximumAllowedSessionsInTrialMode);
}
await inner.Invoke(ctx);
}
return Callback;
}
}

View file

@ -1,8 +1,8 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Duende.Bff.Licensing;
using Duende.Bff.Tests.TestInfra;
using Microsoft.Extensions.Time.Testing;
using Xunit.Abstractions;
namespace Duende.Bff.Tests;
@ -16,37 +16,95 @@ public class LicensingTests(ITestOutputHelper output) : BffTestBase(output)
await InitializeAsync();
var bffLogMessages = Context.LogMessages.ToString().Split(Environment.NewLine).Where(x => x.StartsWith("bff"));
bffLogMessages.ShouldContain(x => x.Contains("You do not have a valid license key for the Duende software."));
bffLogMessages.ShouldContain(x =>
x.Contains("[Error]")
&& x.Contains("You do not have a valid license key for the Duende software."));
}
[Fact]
public async Task Given_expired_license_then_log_error()
public async Task Given_expired_license_then_log_warning()
{
Bff.LicenseKey =
"eyJhbGciOiJQUzI1NiIsImtpZCI6IklkZW50aXR5U2VydmVyTGljZW5zZWtleS83Y2VhZGJiNzgxMzA0NjllODgwNjg5MTAyNTQxNGYxNiIsInR5cCI6ImxpY2Vuc2Urand0In0.eyJpc3MiOiJodHRwczovL2R1ZW5kZXNvZnR3YXJlLmNvbSIsImF1ZCI6IklkZW50aXR5U2VydmVyIiwiaWF0IjoxNzA0MDY3MjAwLCJleHAiOjE3MzE2Mjg4MDAsImNvbXBhbnlfbmFtZSI6Il90ZXN0IiwiY29udGFjdF9pbmZvIjoiam9lQGR1ZW5kZXNvZnR3YXJlLmNvbSIsImVkaXRpb24iOiJTdGFydGVyIiwiaWQiOiI3ODk2IiwiZmVhdHVyZSI6ImJmZiJ9.YcRGLlVuNBSqNuO1mdXk4GvvVEQFfQUNAnTkzs9W2iNKCxLXrZ5mDPuyTNsDSwEqsfXG8bUCVFxFGp1Bfkxs8hUIBiKuVXfeIB_lmpj5f-KueZ_XlWm0pYT-ROAzVbDdNgMR9YqCPAw8ANclk7HwRcXc0VnLNcKRFrZ0OOWNysFIanTmg7hRIQmDuMLNc2j8HCZSRJ06fijecS72lM4Vv9a6myJvAsASQhKnWTLzQvdzW7T99eobLy45qJu39LMTQkPkkJUS41YPmi2_kEmeMcRucgU4dQKHD5zT9KmzPVWJwsyowWIJ6U7lZ8FXZ8c9POsQeTeQEJY6FheJ2Ut-6Q";
SetupExpiredLicense();
await InitializeAsync();
var bffLogMessages = Context.LogMessages.ToString().Split(Environment.NewLine).Where(x => x.StartsWith("bff"));
bffLogMessages.ShouldContain(x => x.Contains("Your license for the Duende Software has expired on "));
bffLogMessages.ShouldContain(x =>
x.Contains("[Warning]")
&& x.Contains("Your license for the Duende software has expired"));
}
[Fact]
public async Task Given_valid_license_then_details()
public async Task Given_valid_but_expired_license_then_no_valid_or_trial_mode_logs()
{
SetupValidLicenseWithoutFrontends();
SetupExpiredLicense();
await InitializeAsync();
AddOrUpdateFrontend(Some.BffFrontend());
for (var i = 0; i <= LicenseValidator.MaximumAllowedSessionsInTrialMode; i++)
{
var subjectId = Guid.NewGuid().ToString();
await Bff.BrowserClient.CreateIdentityServerSessionCookieAsync(IdentityServer, subjectId);
await Bff.BrowserClient.Login();
}
var bffLogMessages = Context.LogMessages.ToString().Split(Environment.NewLine).Where(x => x.StartsWith("bff"))
.ToList();
bffLogMessages.ShouldNotContain(x => x.Contains("You do not have a valid license key for the Duende software."));
bffLogMessages.ShouldNotContain(x => x.Contains("Your license for the Duende Software has expired on "));
bffLogMessages.ShouldNotContain(x =>
x.Contains("You do not have a valid license key for the Duende software."));
bffLogMessages.ShouldContain(x => x.Contains("Your license for the Duende software has expired on "));
bffLogMessages.ShouldNotContain(x =>
x.Contains("BFF is running in trial mode. The maximum number of allowed authenticated sessions "));
}
private void SetupValidLicenseWithoutFrontends()
[Fact]
public async Task Should_not_log_error_when_below_trial_mode_authenticated_session_limit()
{
The.Clock = new FakeTimeProvider(new DateTimeOffset(2024, 1, 1, 1, 1, 1, TimeSpan.Zero));
Bff.LicenseKey =
"eyJhbGciOiJQUzI1NiIsImtpZCI6IklkZW50aXR5U2VydmVyTGljZW5zZWtleS83Y2VhZGJiNzgxMzA0NjllODgwNjg5MTAyNTQxNGYxNiIsInR5cCI6ImxpY2Vuc2Urand0In0.eyJpc3MiOiJodHRwczovL2R1ZW5kZXNvZnR3YXJlLmNvbSIsImF1ZCI6IklkZW50aXR5U2VydmVyIiwiaWF0IjoxNzA0MDY3MjAwLCJleHAiOjE3MzE2Mjg4MDAsImNvbXBhbnlfbmFtZSI6Il90ZXN0IiwiY29udGFjdF9pbmZvIjoiam9lQGR1ZW5kZXNvZnR3YXJlLmNvbSIsImVkaXRpb24iOiJTdGFydGVyIiwiaWQiOiI3ODk2IiwiZmVhdHVyZSI6ImJmZiJ9.YcRGLlVuNBSqNuO1mdXk4GvvVEQFfQUNAnTkzs9W2iNKCxLXrZ5mDPuyTNsDSwEqsfXG8bUCVFxFGp1Bfkxs8hUIBiKuVXfeIB_lmpj5f-KueZ_XlWm0pYT-ROAzVbDdNgMR9YqCPAw8ANclk7HwRcXc0VnLNcKRFrZ0OOWNysFIanTmg7hRIQmDuMLNc2j8HCZSRJ06fijecS72lM4Vv9a6myJvAsASQhKnWTLzQvdzW7T99eobLy45qJu39LMTQkPkkJUS41YPmi2_kEmeMcRucgU4dQKHD5zT9KmzPVWJwsyowWIJ6U7lZ8FXZ8c9POsQeTeQEJY6FheJ2Ut-6Q";
await InitializeAsync();
AddOrUpdateFrontend(Some.BffFrontend());
for (var i = 0; i < LicenseValidator.MaximumAllowedSessionsInTrialMode; i++)
{
var subjectId = Guid.NewGuid().ToString();
await Bff.BrowserClient.CreateIdentityServerSessionCookieAsync(IdentityServer, subjectId);
await Bff.BrowserClient.Login();
}
var bffLogMessages = Context.LogMessages.ToString().Split(Environment.NewLine).Where(x => x.StartsWith("bff"))
.ToList();
bffLogMessages.ShouldNotContain(x =>
x.Contains("[Error]")
&& x.Contains("BFF is running in trial mode. The maximum number of allowed authenticated sessions "));
}
[Fact]
public async Task Should_log_error_when_trial_mode_authenticated_session_limit_exceeded()
{
await InitializeAsync();
AddOrUpdateFrontend(Some.BffFrontend());
for (var i = 0; i < LicenseValidator.MaximumAllowedSessionsInTrialMode + 6; i++)
{
var subjectId = Guid.NewGuid().ToString();
await Bff.BrowserClient.CreateIdentityServerSessionCookieAsync(IdentityServer, subjectId);
await Bff.BrowserClient.Login();
}
var bffLogMessages = Context.LogMessages.ToString().Split(Environment.NewLine).Where(x => x.StartsWith("bff"))
.ToList();
var trialModeLogCount = bffLogMessages.Count(x =>
x.Contains("[Error]")
&& x.Contains("BFF is running in trial mode. The maximum number of allowed authenticated sessions "));
trialModeLogCount.ShouldBe(6);
}
private void SetupExpiredLicense() => Bff.LicenseKey =
"eyJhbGciOiJQUzI1NiIsImtpZCI6IklkZW50aXR5U2VydmVyTGljZW5zZWtleS83Y2VhZGJiNzgxMzA0NjllODgwNjg5MTAyNTQxNGYxNiIsInR5cCI6ImxpY2Vuc2Urand0In0.eyJpc3MiOiJodHRwczovL2R1ZW5kZXNvZnR3YXJlLmNvbSIsImF1ZCI6IklkZW50aXR5U2VydmVyIiwiaWF0IjoxNzA0MDY3MjAwLCJleHAiOjE3MzE2Mjg4MDAsImNvbXBhbnlfbmFtZSI6Il90ZXN0IiwiY29udGFjdF9pbmZvIjoiam9lQGR1ZW5kZXNvZnR3YXJlLmNvbSIsImVkaXRpb24iOiJTdGFydGVyIiwiaWQiOiI3ODk2IiwiZmVhdHVyZSI6ImJmZiJ9.YcRGLlVuNBSqNuO1mdXk4GvvVEQFfQUNAnTkzs9W2iNKCxLXrZ5mDPuyTNsDSwEqsfXG8bUCVFxFGp1Bfkxs8hUIBiKuVXfeIB_lmpj5f-KueZ_XlWm0pYT-ROAzVbDdNgMR9YqCPAw8ANclk7HwRcXc0VnLNcKRFrZ0OOWNysFIanTmg7hRIQmDuMLNc2j8HCZSRJ06fijecS72lM4Vv9a6myJvAsASQhKnWTLzQvdzW7T99eobLy45qJu39LMTQkPkkJUS41YPmi2_kEmeMcRucgU4dQKHD5zT9KmzPVWJwsyowWIJ6U7lZ8FXZ8c9POsQeTeQEJY6FheJ2Ut-6Q";
}