diff --git a/bff/src/Bff/BffBuilderExtensions.cs b/bff/src/Bff/BffBuilderExtensions.cs index 1f0263944..b9a33621f 100644 --- a/bff/src/Bff/BffBuilderExtensions.cs +++ b/bff/src/Bff/BffBuilderExtensions.cs @@ -97,6 +97,11 @@ public static class BffBuilderExtensions builder.Services .AddSingleton, PostConfigureOidcOptionsForSilentLogin>(); + builder.Services.AddSingleton(); + builder.Services + .AddSingleton, + PostConfigureApplicationCookieTrialModeCheck>(); + AddBffMetrics(builder); // wrap ASP.NET Core diff --git a/bff/src/Bff/Licensing/LicenseAccessor.cs b/bff/src/Bff/Licensing/LicenseAccessor.cs index cfd4cfa79..e6183fe92 100644 --- a/bff/src/Bff/Licensing/LicenseAccessor.cs +++ b/bff/src/Bff/Licensing/LicenseAccessor.cs @@ -44,7 +44,10 @@ internal class LicenseAccessor(GetLicenseKey getLicenseKey, ILogger logger, License licens } private bool? _licenseCheckResult; + internal const int MaximumAllowedSessionsInTrialMode = 5; public bool CheckLicense() { @@ -34,13 +35,19 @@ internal class LicenseValidator(ILogger 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; } } diff --git a/bff/src/Bff/Licensing/LicensingLogMessages.cs b/bff/src/Bff/Licensing/LicensingLogMessages.cs index cbb91f96a..6f6c65ca6 100644 --- a/bff/src/Bff/Licensing/LicensingLogMessages.cs +++ b/bff/src/Bff/Licensing/LicensingLogMessages.cs @@ -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); // } diff --git a/bff/src/Bff/Licensing/TrialModeAuthenticatedSessionTracker.cs b/bff/src/Bff/Licensing/TrialModeAuthenticatedSessionTracker.cs new file mode 100644 index 000000000..8de94f481 --- /dev/null +++ b/bff/src/Bff/Licensing/TrialModeAuthenticatedSessionTracker.cs @@ -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 _authenticatedSessions = new(); + + public int UniqueAuthenticatedSessions => _authenticatedSessions.Count; + + public void RecordAuthenticatedSession(string subjectId) + { + if (_authenticatedSessions.Count <= LicenseValidator.MaximumAllowedSessionsInTrialMode) + { + _authenticatedSessions.TryAdd(subjectId, 0); + } + } +} diff --git a/bff/src/Bff/Otel/Generated/Microsoft.Gen.Logging/Microsoft.Gen.Logging.LoggingGenerator/Logging.g.cs b/bff/src/Bff/Otel/Generated/Microsoft.Gen.Logging/Microsoft.Gen.Logging.LoggingGenerator/Logging.g.cs index 74aa4e6c2..5dd86ecec 100644 --- a/bff/src/Bff/Otel/Generated/Microsoft.Gen.Logging/Microsoft.Gen.Logging.LoggingGenerator/Logging.g.cs +++ b/bff/src/Bff/Otel/Generated/Microsoft.Gen.Logging/Microsoft.Gen.Logging.LoggingGenerator/Logging.g.cs @@ -90,10 +90,10 @@ namespace Duende.Bff.Licensing internal static class LicensingLogMessages { /// - /// 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}". /// [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 } /// - /// 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.". /// [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 } /// - /// 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"". /// [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(); } /// - /// 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.". /// [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(); } - - /// - /// Logs "Frontend #{FrontendsUsed} with name {FrontendName} was added. The license allows for unlimited frontends.". - /// - [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(); - } - - /// - /// Logs "Frontend {FrontendName} was added. Currently using {frontendsUsed} of {frontendLimit} in the BFF License.". - /// - [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(); - } - - /// - /// 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.". - /// - [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(); - } - - /// - /// 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.". - /// - [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); diff --git a/bff/src/Bff/Otel/LogMessages.cs b/bff/src/Bff/Otel/LogMessages.cs index 227d2492b..a97bce324 100644 --- a/bff/src/Bff/Otel/LogMessages.cs +++ b/bff/src/Bff/Otel/LogMessages.cs @@ -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); diff --git a/bff/src/Bff/SessionManagement/Configuration/PostConfigureApplicationCookieTrialModeCheck.cs b/bff/src/Bff/SessionManagement/Configuration/PostConfigureApplicationCookieTrialModeCheck.cs new file mode 100644 index 000000000..c1172ea3c --- /dev/null +++ b/bff/src/Bff/SessionManagement/Configuration/PostConfigureApplicationCookieTrialModeCheck.cs @@ -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; + +/// +/// Cookie configuration to check license validity when a user authenticates. +/// +internal class PostConfigureApplicationCookieTrialModeCheck( + ActiveCookieAuthenticationScheme activeCookieScheme, + LicenseValidator licenseValidator, + TrialModeAuthenticatedSessionTracker authenticatedSessionTracker, + ILogger logger) + : IPostConfigureOptions +{ + /// + 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 CreateCallback(Func 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; + } +} diff --git a/bff/test/Bff.Tests/LicensingTests.cs b/bff/test/Bff.Tests/LicensingTests.cs index 59af03634..a96a98b3d 100644 --- a/bff/test/Bff.Tests/LicensingTests.cs +++ b/bff/test/Bff.Tests/LicensingTests.cs @@ -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"; }