diff --git a/.gitignore b/.gitignore index 4918295c7..10a11b58d 100644 --- a/.gitignore +++ b/.gitignore @@ -228,4 +228,4 @@ artifacts *.Artifacts/ -reports \ No newline at end of file +reports diff --git a/identity-server/src/IdentityServer/Configuration/DependencyInjection/BuilderExtensions/Core.cs b/identity-server/src/IdentityServer/Configuration/DependencyInjection/BuilderExtensions/Core.cs index 874f1815a..50ed0d3b0 100644 --- a/identity-server/src/IdentityServer/Configuration/DependencyInjection/BuilderExtensions/Core.cs +++ b/identity-server/src/IdentityServer/Configuration/DependencyInjection/BuilderExtensions/Core.cs @@ -228,7 +228,11 @@ public static class IdentityServerBuilderExtensionsCore builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(serviceProvider => new DiagnosticSummary( + DateTime.UtcNow, + serviceProvider.GetServices(), + serviceProvider.GetRequiredService(), + serviceProvider.GetRequiredService())); builder.Services.AddHostedService(); return builder; diff --git a/identity-server/src/IdentityServer/Configuration/DependencyInjection/Options/LoggingOptions.cs b/identity-server/src/IdentityServer/Configuration/DependencyInjection/Options/LoggingOptions.cs index 796416585..900d001f3 100644 --- a/identity-server/src/IdentityServer/Configuration/DependencyInjection/Options/LoggingOptions.cs +++ b/identity-server/src/IdentityServer/Configuration/DependencyInjection/Options/LoggingOptions.cs @@ -50,6 +50,9 @@ public class LoggingOptions public ICollection AuthorizeRequestSensitiveValuesFilter { get; set; } = new HashSet { + // Secrets and assertions may be passed to the authorize endpoint via PAR + OidcConstants.TokenRequest.ClientSecret, + OidcConstants.TokenRequest.ClientAssertion, OidcConstants.AuthorizeRequest.IdTokenHint, OidcConstants.AuthorizeRequest.Request }; @@ -58,11 +61,15 @@ public class LoggingOptions /// Gets or sets the collection of keys that will be used to redact sensitive values from a pushed authorization request log. /// /// Please be aware that initializing this property could expose sensitive information in your logs. + /// Note that pushed authorization parameters are eventually handled by the authorize request pipeline. + /// In most cases, changes to this collection should also be made to + /// public ICollection PushedAuthorizationSensitiveValuesFilter { get; set; } = new HashSet { OidcConstants.TokenRequest.ClientSecret, OidcConstants.TokenRequest.ClientAssertion, + OidcConstants.AuthorizeRequest.IdTokenHint, OidcConstants.AuthorizeRequest.Request }; diff --git a/identity-server/src/IdentityServer/Configuration/DependencyInjection/Options/MtlsOptions.cs b/identity-server/src/IdentityServer/Configuration/DependencyInjection/Options/MtlsOptions.cs index 72faeb89a..e6971e714 100644 --- a/identity-server/src/IdentityServer/Configuration/DependencyInjection/Options/MtlsOptions.cs +++ b/identity-server/src/IdentityServer/Configuration/DependencyInjection/Options/MtlsOptions.cs @@ -23,9 +23,18 @@ public class MutualTlsOptions /// /// Specifies a separate domain to run the MTLS endpoints on. - /// If the string does not contain any dots, a subdomain is assumed - e.g. main domain: identityserver.local, MTLS domain: mtls.identityserver.local - /// If the string contains dots, a completely separate domain is assumend, e.g. main domain: identity.app.com, MTLS domain: mtls.app.com. In this case you must set a static issuer name on the options. /// + /// If the string does not contain any dots, it is treated as a + /// subdomain. For example, if the non-mTLS endpoints are hosted at + /// example.com, configuring this option with the value "mtls" means that + /// mtls is required for requests to mtls.example.com. + /// + /// If the string contains dots, it is treated as a complete domain. + /// mTLS will be required for requests whose host name matches the + /// configured domain name completely, including the port number. + /// This allows for separate domains for the mTLS and non-mTLS endpoints. + /// For example, identity.example.com and mtls.example.com. + /// public string? DomainName { get; set; } /// diff --git a/identity-server/src/IdentityServer/Endpoints/Results/ProtectedResourceErrorResult.cs b/identity-server/src/IdentityServer/Endpoints/Results/ProtectedResourceErrorResult.cs index 94b9cf76b..fd2e3f564 100644 --- a/identity-server/src/IdentityServer/Endpoints/Results/ProtectedResourceErrorResult.cs +++ b/identity-server/src/IdentityServer/Endpoints/Results/ProtectedResourceErrorResult.cs @@ -2,7 +2,6 @@ // See LICENSE in the project root for license information. -using System.Globalization; using Duende.IdentityModel; using Duende.IdentityServer.Extensions; using Duende.IdentityServer.Hosting; @@ -59,17 +58,25 @@ internal class ProtectedResourceErrorHttpWriter : IHttpResponseWriter { - context.Response.Headers.Append(HeaderNames.WWWAuthenticate, new StringValues(new[] { "Bearer realm=\"IdentityServer\"", errorString })); - } - else + """ + Bearer realm="IdentityServer" + """, + $""" + error="{error}" + """ + }; + + if (!errorDescription.IsMissing()) { - var errorDescriptionString = string.Format(CultureInfo.InvariantCulture, $"error_description=\"{errorDescription}\""); - context.Response.Headers.Append(HeaderNames.WWWAuthenticate, new StringValues(new[] { "Bearer realm=\"IdentityServer\"", errorString, errorDescriptionString })); + values.Add($""" + error_description="{errorDescription}" + """); } + context.Response.Headers.Append(HeaderNames.WWWAuthenticate, string.Join(",", values)); + return Task.CompletedTask; } } diff --git a/identity-server/src/IdentityServer/Hosting/MutualTlsEndpointMiddleware.cs b/identity-server/src/IdentityServer/Hosting/MutualTlsEndpointMiddleware.cs index 71f1209a9..5e5d8581a 100644 --- a/identity-server/src/IdentityServer/Hosting/MutualTlsEndpointMiddleware.cs +++ b/identity-server/src/IdentityServer/Hosting/MutualTlsEndpointMiddleware.cs @@ -35,64 +35,116 @@ public class MutualTlsEndpointMiddleware _sanitizedLogger = new SanitizedLogger(logger); } + internal enum MtlsEndpointType + { + None, + SeparateDomain, + Subdomain, + PathBased + } + + internal MtlsEndpointType DetermineMtlsEndpointType(HttpContext context, out PathString? subPath) + { + subPath = null; + + if (!_options.MutualTls.Enabled) + { + return MtlsEndpointType.None; + } + + if (_options.MutualTls.DomainName.IsPresent()) + { + if (_options.MutualTls.DomainName.Contains('.', StringComparison.InvariantCulture)) + { + var requestedHost = HostString.FromUriComponent(_options.MutualTls.DomainName); + // Separate domain + if (RequestedHostMatches(context.Request.Host, _options.MutualTls.DomainName)) + { + _sanitizedLogger.LogDebug("Requiring mTLS because the request's domain matches the configured mTLS domain name."); + return MtlsEndpointType.SeparateDomain; + } + } + else + { + // Subdomain + if (context.Request.Host.Host.StartsWith(_options.MutualTls.DomainName + ".", StringComparison.OrdinalIgnoreCase)) + { + _sanitizedLogger.LogDebug("Requiring mTLS because the request's subdomain matches the configured mTLS domain name."); + return MtlsEndpointType.Subdomain; + } + } + + _sanitizedLogger.LogDebug("Not requiring mTLS because this request's domain does not match the configured mTLS domain name."); + return MtlsEndpointType.None; + } + + // Check path-based MTLS + if (context.Request.Path.StartsWithSegments( + ProtocolRoutePaths.MtlsPathPrefix.EnsureLeadingSlash(), out var path)) + { + _sanitizedLogger.LogDebug("Requiring mTLS because the request's path begins with the configured mTLS path prefix."); + subPath = path; + return MtlsEndpointType.PathBased; + } + + return MtlsEndpointType.None; + } + /// public async Task Invoke(HttpContext context, IAuthenticationSchemeProvider schemes) { - if (_options.MutualTls.Enabled) + var mtlsConfigurationStyle = DetermineMtlsEndpointType(context, out var subPath); + + if (mtlsConfigurationStyle != MtlsEndpointType.None) { - // domain-based MTLS - if (_options.MutualTls.DomainName.IsPresent()) + var result = await TriggerCertificateAuthentication(context); + if (!result.Succeeded) { - // separate domain - if (_options.MutualTls.DomainName.Contains('.', StringComparison.InvariantCulture)) - { - if (context.Request.Host.Host.Equals(_options.MutualTls.DomainName, - StringComparison.OrdinalIgnoreCase)) - { - var result = await TriggerCertificateAuthentication(context); - if (!result.Succeeded) - { - return; - } - } - } - // sub-domain - else - { - if (context.Request.Host.Host.StartsWith(_options.MutualTls.DomainName + ".", StringComparison.OrdinalIgnoreCase)) - { - var result = await TriggerCertificateAuthentication(context); - if (!result.Succeeded) - { - return; - } - } - } + return; } - // path based MTLS - else if (context.Request.Path.StartsWithSegments(ProtocolRoutePaths.MtlsPathPrefix.EnsureLeadingSlash(), out var subPath)) + + // Additional processing for path-based MTLS + if (mtlsConfigurationStyle == MtlsEndpointType.PathBased && subPath.HasValue) { - var result = await TriggerCertificateAuthentication(context); + var path = ProtocolRoutePaths.ConnectPathPrefix + subPath.Value.ToString().EnsureLeadingSlash(); + path = path.EnsureLeadingSlash(); - if (result.Succeeded) - { - var path = ProtocolRoutePaths.ConnectPathPrefix + subPath.ToString().EnsureLeadingSlash(); - path = path.EnsureLeadingSlash(); - - _sanitizedLogger.LogDebug("Rewriting MTLS request from: {oldPath} to: {newPath}", - context.Request.Path.ToString(), path); - context.Request.Path = path; - } - else - { - return; - } + _sanitizedLogger.LogDebug("Rewriting MTLS request from: {oldPath} to: {newPath}", + context.Request.Path.ToString(), path); + context.Request.Path = path; } } await _next(context); } + + private static bool RequestedHostMatches(HostString requestHost, string configuredDomain) + { + // Parse the configured domain which might contain a port + var configuredHostname = configuredDomain; + var configuredPort = 443; + + var colonIndex = configuredDomain.IndexOf(':', StringComparison.InvariantCulture); + if (colonIndex >= 0) + { + configuredHostname = configuredDomain.Substring(0, colonIndex); + if (int.TryParse(configuredDomain.AsSpan(colonIndex + 1), out var port)) + { + configuredPort = port; + } + } + + // Compare hostnames (case-insensitive) + if (!string.Equals(requestHost.Host, configuredHostname, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + var requestPort = requestHost.Port ?? 443; + return requestPort == configuredPort; + } + private async Task TriggerCertificateAuthentication(HttpContext context) { var x509AuthResult = await context.AuthenticateAsync(_options.MutualTls.ClientCertificateAuthenticationScheme); diff --git a/identity-server/src/IdentityServer/Licensing/IdentityServerLicenseValidator.cs b/identity-server/src/IdentityServer/Licensing/IdentityServerLicenseValidator.cs index 21117fc95..ce0404da5 100644 --- a/identity-server/src/IdentityServer/Licensing/IdentityServerLicenseValidator.cs +++ b/identity-server/src/IdentityServer/Licensing/IdentityServerLicenseValidator.cs @@ -105,13 +105,14 @@ internal class IdentityServerLicenseValidator : LicenseValidator license.ClientLimit) { ErrorLog.Invoke( - "Your license for Duende IdentityServer only permits {clientLimit} number of clients. You have processed requests for {clientCount}. The clients used were: {clients}.", - [license.ClientLimit, _clientIds.Count, _clientIds.ToArray()]); + "Your license for IdentityServer includes {clientLimit} clients but you have processed requests for {clientCount} clients. Please contact {contactInfo} at {companyName} or start a conversation with us at https://duende.link/l/contact to upgrade your license as soon as possible. In a future version, this limit will be enforced after a threshold is exceeded. The clients used were: {clients}.", + [license.ClientLimit, _clientIds.Count, license.ContactInfo, license.CompanyName, _clientIds.ToArray()]); } } } @@ -131,12 +132,13 @@ internal class IdentityServerLicenseValidator : LicenseValidator license.IssuerLimit) { ErrorLog.Invoke( - "Your license for Duende IdentityServer only permits {issuerLimit} number of issuers. You have processed requests for {issuerCount}. The issuers used were: {issuers}. This might be due to your server being accessed via different URLs or a direct IP and/or you have reverse proxy or a gateway involved. This suggests a network infrastructure configuration problem, or you are deliberately hosting multiple URLs and require an upgraded license.", + "Your license for IdentityServer includes {issuerLimit} issuers but you have processed requests for {issuerCount} issuers. This indicates that requests for each issuer are being sent to this instance of IdentityServer, which may be due to a network infrastructure configuration issue. If you intend to use multiple issuers, please contact {contactInfo} at {companyName} or start a conversation with us at https://duende.link/l/contact to upgrade your license as soon as possible. In a future version, this limit will be enforced after a threshold is exceeded. The issuers used were {issuers}.", [license.IssuerLimit, _issuers.Count, _issuers.ToArray()]); } } diff --git a/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticContext.cs b/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticContext.cs new file mode 100644 index 000000000..0552ff033 --- /dev/null +++ b/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticContext.cs @@ -0,0 +1,6 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + +namespace Duende.IdentityServer.Licensing.V2.Diagnostics; + +public record DiagnosticContext(DateTime ServerStartTime, DateTime CurrentSeverTime); diff --git a/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/AssemblyInfoDiagnosticEntry.cs b/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/AssemblyInfoDiagnosticEntry.cs index 5692161c9..9749b13be 100644 --- a/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/AssemblyInfoDiagnosticEntry.cs +++ b/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/AssemblyInfoDiagnosticEntry.cs @@ -39,7 +39,7 @@ internal class AssemblyInfoDiagnosticEntry : IDiagnosticEntry _startsWithMatches = startsWithMatches ?? _defaultStartsWithMatches; } - public Task WriteAsync(Utf8JsonWriter writer) + public Task WriteAsync(DiagnosticContext context, Utf8JsonWriter writer) { var assemblies = GetAssemblyInfo(); writer.WriteStartObject("AssemblyInfo"); diff --git a/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/AuthSchemeInfoDiagnosticEntry.cs b/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/AuthSchemeInfoDiagnosticEntry.cs index 70af5a8f8..40277db4e 100644 --- a/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/AuthSchemeInfoDiagnosticEntry.cs +++ b/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/AuthSchemeInfoDiagnosticEntry.cs @@ -8,7 +8,7 @@ namespace Duende.IdentityServer.Licensing.V2.Diagnostics.DiagnosticEntries; internal class AuthSchemeInfoDiagnosticEntry(IAuthenticationSchemeProvider authenticationSchemeProvider) : IDiagnosticEntry { - public async Task WriteAsync(Utf8JsonWriter writer) + public async Task WriteAsync(DiagnosticContext context, Utf8JsonWriter writer) { var schemes = await authenticationSchemeProvider.GetAllSchemesAsync(); diff --git a/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/BasicServerInfoDiagnosticEntry.cs b/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/BasicServerInfoDiagnosticEntry.cs index ba5ad55a2..2f4d7db17 100644 --- a/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/BasicServerInfoDiagnosticEntry.cs +++ b/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/BasicServerInfoDiagnosticEntry.cs @@ -7,11 +7,13 @@ namespace Duende.IdentityServer.Licensing.V2.Diagnostics.DiagnosticEntries; internal class BasicServerInfoDiagnosticEntry(Func hostNameResolver) : IDiagnosticEntry { - public Task WriteAsync(Utf8JsonWriter writer) + public Task WriteAsync(DiagnosticContext context, Utf8JsonWriter writer) { writer.WriteStartObject("BasicServerInfo"); writer.WriteString("HostName", hostNameResolver()); + writer.WriteString("ServerStartTime", context.ServerStartTime.ToString("o")); + writer.WriteString("CurrentServerTime", context.CurrentSeverTime.ToString("o")); writer.WriteEndObject(); diff --git a/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/ClientInfoDiagnosticEntry.cs b/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/ClientInfoDiagnosticEntry.cs index 122d5f60a..227344547 100644 --- a/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/ClientInfoDiagnosticEntry.cs +++ b/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/ClientInfoDiagnosticEntry.cs @@ -13,7 +13,7 @@ internal class ClientInfoDiagnosticEntry(ClientLoadedTracker clientLoadedTracker WriteIndented = false }; - public Task WriteAsync(Utf8JsonWriter writer) + public Task WriteAsync(DiagnosticContext context, Utf8JsonWriter writer) { writer.WriteStartArray("Clients"); diff --git a/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/DataProtectionDiagnosticEntry.cs b/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/DataProtectionDiagnosticEntry.cs index 571e47e61..917bd1029 100644 --- a/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/DataProtectionDiagnosticEntry.cs +++ b/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/DataProtectionDiagnosticEntry.cs @@ -10,7 +10,7 @@ namespace Duende.IdentityServer.Licensing.V2.Diagnostics.DiagnosticEntries; internal class DataProtectionDiagnosticEntry(IOptions dataProtectionOptions, IOptions keyManagementOptions) : IDiagnosticEntry { - public Task WriteAsync(Utf8JsonWriter writer) + public Task WriteAsync(DiagnosticContext context, Utf8JsonWriter writer) { writer.WriteStartObject("DataProtectionConfiguration"); writer.WriteString("ApplicationDiscriminator", dataProtectionOptions?.Value?.ApplicationDiscriminator ?? "Not Configured"); diff --git a/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/EndpointUsageDiagnosticEntry.cs b/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/EndpointUsageDiagnosticEntry.cs index b9857adbd..4f3d02bef 100644 --- a/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/EndpointUsageDiagnosticEntry.cs +++ b/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/EndpointUsageDiagnosticEntry.cs @@ -45,7 +45,7 @@ internal class EndpointUsageDiagnosticEntry : IDiagnosticEntry, IDisposable _meterListener.Start(); } - public Task WriteAsync(Utf8JsonWriter writer) + public Task WriteAsync(DiagnosticContext context, Utf8JsonWriter writer) { writer.WriteStartObject("EndpointUsage"); diff --git a/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/IdentityServerOptionsDiagnosticEntry.cs b/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/IdentityServerOptionsDiagnosticEntry.cs index 0bd818446..0d65a1b38 100644 --- a/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/IdentityServerOptionsDiagnosticEntry.cs +++ b/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/IdentityServerOptionsDiagnosticEntry.cs @@ -23,7 +23,7 @@ internal class IdentityServerOptionsDiagnosticEntry(IOptions entries, IdentityServerOptions options, ILoggerFactory loggerFactory) +internal class DiagnosticSummary(DateTime serverStartTime, IEnumerable entries, IdentityServerOptions options, ILoggerFactory loggerFactory) { private readonly ILogger _logger = loggerFactory.CreateLogger("Duende.IdentityServer.Diagnostics.Summary"); + public async Task PrintSummary() { var bufferWriter = new ArrayBufferWriter(); @@ -19,9 +20,10 @@ internal class DiagnosticSummary(IEnumerable entries, Identity writer.WriteStartObject(); + var diagnosticContext = new DiagnosticContext(serverStartTime, DateTime.UtcNow); foreach (var diagnosticEntry in entries) { - await diagnosticEntry.WriteAsync(writer); + await diagnosticEntry.WriteAsync(diagnosticContext, writer); } writer.WriteEndObject(); diff --git a/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/IDiagnosticEntry.cs b/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/IDiagnosticEntry.cs index 22114e69a..a44640da4 100644 --- a/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/IDiagnosticEntry.cs +++ b/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/IDiagnosticEntry.cs @@ -7,5 +7,5 @@ namespace Duende.IdentityServer.Licensing.V2.Diagnostics; internal interface IDiagnosticEntry { - Task WriteAsync(Utf8JsonWriter writer); + public Task WriteAsync(DiagnosticContext context, Utf8JsonWriter writer); } diff --git a/identity-server/src/IdentityServer/Licensing/V2/LicenseExpirationChecker.cs b/identity-server/src/IdentityServer/Licensing/V2/LicenseExpirationChecker.cs index a0384b2c3..e248537fb 100644 --- a/identity-server/src/IdentityServer/Licensing/V2/LicenseExpirationChecker.cs +++ b/identity-server/src/IdentityServer/Licensing/V2/LicenseExpirationChecker.cs @@ -21,7 +21,7 @@ internal class LicenseExpirationChecker( if (!_expiredLicenseWarned && !license.Current.Redistribution && IsExpired) { _expiredLicenseWarned = true; - _logger.LicenseHasExpired(); + _logger.LicenseHasExpired(license.Current.ContactInfo ?? "", license.Current.CompanyName ?? ""); } } diff --git a/identity-server/src/IdentityServer/Licensing/V2/LicenseUsageTracker.cs b/identity-server/src/IdentityServer/Licensing/V2/LicenseUsageTracker.cs index 006bc5566..67c16b3ce 100644 --- a/identity-server/src/IdentityServer/Licensing/V2/LicenseUsageTracker.cs +++ b/identity-server/src/IdentityServer/Licensing/V2/LicenseUsageTracker.cs @@ -55,21 +55,23 @@ internal class LicenseUsageTracker(LicenseAccessor licenseAccessor, ILoggerFacto return; } - if (licenseAccessor.Current.IsConfigured) + var license = licenseAccessor.Current; + + if (license.IsConfigured) { - if (licenseAccessor.Current.Redistribution || !licenseAccessor.Current.ClientLimit.HasValue) + if (license.Redistribution || !license.ClientLimit.HasValue) { return; } - var clientLimitOverage = _clientsUsed.Values.Count - licenseAccessor.Current.ClientLimit; + var clientLimitOverage = _clientsUsed.Values.Count - license.ClientLimit; switch (clientLimitOverage) { case > ClientLimitExceededThreshold: - _logger.ClientLimitExceededOverThreshold(licenseAccessor.Current.ClientLimit.Value, _clientsUsed.Values.Count, ClientLimitExceededThreshold, _clientsUsed.Values); + _logger.ClientLimitExceededOverThreshold(license.ClientLimit.Value, _clientsUsed.Values.Count, license.ContactInfo, license.CompanyName, _clientsUsed.Values); break; case > 0: - _logger.ClientLimitExceededWithinOverageThreshold(licenseAccessor.Current.ClientLimit.Value, _clientsUsed.Values.Count, ClientLimitExceededThreshold, _clientsUsed.Values); + _logger.ClientLimitExceededWithinOverageThreshold(license.ClientLimit.Value, _clientsUsed.Values.Count, license.ContactInfo, license.CompanyName, _clientsUsed.Values); break; } } @@ -93,21 +95,23 @@ internal class LicenseUsageTracker(LicenseAccessor licenseAccessor, ILoggerFacto return; } - if (licenseAccessor.Current.IsConfigured) + var license = licenseAccessor.Current; + + if (license.IsConfigured) { - if (licenseAccessor.Current.Redistribution || !licenseAccessor.Current.IssuerLimit.HasValue) + if (license.Redistribution || !license.IssuerLimit.HasValue) { return; } - var issuerLimitOverage = _issuersUsed.Values.Count - licenseAccessor.Current.IssuerLimit; + var issuerLimitOverage = _issuersUsed.Values.Count - license.IssuerLimit; switch (issuerLimitOverage) { case > IssuerLimitExceededThreshold: - _logger.IssuerLimitExceededOverThreshold(licenseAccessor.Current.IssuerLimit.Value, _issuersUsed.Values.Count, IssuerLimitExceededThreshold, _issuersUsed.Values); + _logger.IssuerLimitExceededOverThreshold(license.IssuerLimit.Value, _issuersUsed.Values.Count, license.ContactInfo, license.CompanyName, _issuersUsed.Values); break; case > 0: - _logger.IssuerLimitExceededWithinOverageThreshold(licenseAccessor.Current.IssuerLimit.Value, _issuersUsed.Values.Count, IssuerLimitExceededThreshold, _issuersUsed.Values); + _logger.IssuerLimitExceededWithinOverageThreshold(license.IssuerLimit.Value, _issuersUsed.Values.Count, license.ContactInfo, license.CompanyName, _issuersUsed.Values); break; } } diff --git a/identity-server/src/IdentityServer/Licensing/V2/Log.cs b/identity-server/src/IdentityServer/Licensing/V2/Log.cs index 5aa8e1a11..95a0d9deb 100644 --- a/identity-server/src/IdentityServer/Licensing/V2/Log.cs +++ b/identity-server/src/IdentityServer/Licensing/V2/Log.cs @@ -10,12 +10,12 @@ internal static class LicenseLogParameters public const string Threshold = "Threshold"; public const string ClientLimit = "ClientLimit"; public const string ClientCount = "ClientCount"; - public const string ClientLimitExceededThreshold = "ClientLimitExceededThreshold"; public const string ClientsUsed = "ClientsUsed"; public const string IssuerLimit = "IssuerLimit"; public const string IssuerCount = "IssuerCount"; - public const string IssuerLimitExceededThreshold = "IssuerLimitExceededThreshold"; public const string IssuersUsed = "IssuersUsed"; + public const string LicenseContact = "LicenseContact"; + public const string LicenseCompany = "LicenseCompany"; } internal static partial class Log @@ -27,47 +27,54 @@ internal static partial class Log [LoggerMessage( LogLevel.Error, - message: "The IdentityServer license is expired. In a future version of IdentityServer, license expiration will be enforced after a grace period.")] - public static partial void LicenseHasExpired(this ILogger logger); + message: $"Your IdentityServer license is expired. Please contact {{{LicenseLogParameters.LicenseContact}}} from {{{LicenseLogParameters.LicenseCompany}}} or start a conversation with us at https://duende.link/l/contact to renew your license as soon as possible. In a future version, license expiration will be enforced after a grace period. See https://duende.link/l/expired for more information.")] + public static partial void LicenseHasExpired(this ILogger logger, + string licenseContact, string licenseCompany); [LoggerMessage( LogLevel.Error, Message = - $"You are using IdentityServer in trial mode and have exceeded the trial threshold of {{{LicenseLogParameters.Threshold}}} requests handled by IdentityServer. In a future version, you will need to restart the server or configure a license key to continue testing. For more information, please see http://duende.link/trialmode.")] + $"You are using IdentityServer in trial mode and have exceeded the trial threshold of {{{LicenseLogParameters.Threshold}}} requests handled by IdentityServer. In a future version, you will need to restart the server or configure a license key to continue testing. See https://duende.link/l/trial for more information.")] public static partial void TrialModeRequestCountExceeded(this ILogger logger, ulong threshold); [LoggerMessage( LogLevel.Error, - message: $"Your license for Duende IdentityServer only permits {{{LicenseLogParameters.ClientLimit}}} number of clients. You have processed requests for {{{LicenseLogParameters.ClientCount}}} clients and are still within the threshold of {{{LicenseLogParameters.ClientLimitExceededThreshold}}} for exceeding permitted clients. In a future version of client limit will be enforced. The clients used were: {{{LicenseLogParameters.ClientsUsed}}}.")] - public static partial void ClientLimitExceededWithinOverageThreshold(this ILogger logger, int clientLimit, - int clientCount, int clientLimitExceededThreshold, IReadOnlyCollection clientsUsed); + message: + $"Your IdentityServer license includes {{{LicenseLogParameters.ClientLimit}}} clients but you have processed requests for {{{LicenseLogParameters.ClientCount}}} clients. Please contact {{{LicenseLogParameters.LicenseContact}}} from {{{LicenseLogParameters.LicenseCompany}}} or start a conversation with us at https://duende.link/l/contact to upgrade your license as soon as possible. In a future version, this limit will be enforced after a threshold is exceeded. The clients used were: {{{LicenseLogParameters.ClientsUsed}}}. See https://duende.link/l/threshold for more information.")] + public static partial void ClientLimitExceededWithinOverageThreshold(this ILogger logger, + int clientLimit, int clientCount, string licenseContact, string licenseCompany, IReadOnlyCollection clientsUsed); + + // Language is deliberately the same when over or under threshold (will change in future version). + [LoggerMessage( + LogLevel.Error, + message: + $"Your IdentityServer license includes {{{LicenseLogParameters.ClientLimit}}} clients but you have processed requests for {{{LicenseLogParameters.ClientCount}}} clients. Please contact {{{LicenseLogParameters.LicenseContact}}} from {{{LicenseLogParameters.LicenseCompany}}} or start a conversation with us at https://duende.link/l/contact to upgrade your license as soon as possible. In a future version, this limit will be enforced after a threshold is exceeded. The clients used were: {{{LicenseLogParameters.ClientsUsed}}}. See https://duende.link/l/threshold for more information.")] + public static partial void ClientLimitExceededOverThreshold(this ILogger logger, + int clientLimit, int clientCount, string licenseContact, string licenseCompany, IReadOnlyCollection clientsUsed); [LoggerMessage( LogLevel.Error, message: - $"Your license for Duende IdentityServer only permits {{{LicenseLogParameters.ClientLimit}}} number of clients. You have processed requests for {{{LicenseLogParameters.ClientCount}}} clients and are beyond the threshold of {{{LicenseLogParameters.ClientLimitExceededThreshold}}} for exceeding permitted clients. In a future version of client limit will be enforced. The clients used were: {{{LicenseLogParameters.ClientsUsed}}}.")] - public static partial void ClientLimitExceededOverThreshold(this ILogger logger, int clientLimit, int clientCount, - int clientLimitExceededThreshold, IReadOnlyCollection clientsUsed); - - [LoggerMessage( - LogLevel.Error, - message: - $"You do not have a license, and you have processed requests for {{{LicenseLogParameters.ClientCount}}} clients. This number requires a tier of license higher than Starter Edition. The clients used were: {{{LicenseLogParameters.ClientsUsed}}}.")] + $"You are using IdentityServer in trial mode and have processed requests for {{{LicenseLogParameters.ClientCount}}} clients. In production, this will require a license with sufficient client capacity. You can either purchase a license tier that includes this many clients or add additional client capacity to a Starter Edition license. The clients used were: {{{LicenseLogParameters.ClientsUsed}}}. See https://duende.link/l/trial for more information.")] public static partial void ClientLimitWithNoLicenseExceeded(this ILogger logger, int clientCount, IReadOnlyCollection clientsUsed); [LoggerMessage( LogLevel.Error, - message: $"Your license for Duende IdentityServer only permits {{{LicenseLogParameters.IssuerLimit}}} number of issuers. You have processed requests for {{{LicenseLogParameters.IssuerCount}}} issuers and are still within the threshold of {{{LicenseLogParameters.IssuerLimitExceededThreshold}}}. The issuers used were: {{{LicenseLogParameters.IssuersUsed}}}. This might be due to your server being accessed via different URLs or a direct IP and/or you have reverse proxy or a gateway involved. This suggests a network infrastructure configuration problem, or you are deliberately hosting multiple URLs and require an upgraded license. In a future version of issuer limit will be enforced.")] - public static partial void IssuerLimitExceededWithinOverageThreshold(this ILogger logger, int issuerLimit, int issuerCount, int issuerLimitExceededThreshold, IReadOnlyCollection issuersUsed); + message: $"Your license for IdentityServer includes {{{LicenseLogParameters.IssuerLimit}}} issuer(s) but you have processed requests for {{{LicenseLogParameters.IssuerCount}}} issuers. This indicates that requests for each issuer are being sent to this instance of IdentityServer, which may be due to a network infrastructure configuration issue. If you intend to use multiple issuers, please contact {{{LicenseLogParameters.LicenseContact}}} from {{{LicenseLogParameters.LicenseCompany}}} or start a conversation with us at https://duende.link/l/contact to upgrade your license as soon as possible. In a future version, this limit will be enforced after a threshold is exceeded. The issuers used were {{{LicenseLogParameters.IssuersUsed}}}. See https://duende.link/l/threshold for more information.")] + public static partial void IssuerLimitExceededWithinOverageThreshold(this ILogger logger, + int issuerLimit, int issuerCount, string licenseContact, string licenseCompany, IReadOnlyCollection issuersUsed); + + // Language is deliberately the same when over or under threshold (will change in future version). + [LoggerMessage( + LogLevel.Error, + message: $"Your license for IdentityServer includes {{{LicenseLogParameters.IssuerLimit}}} issuer(s) but you have processed requests for {{{LicenseLogParameters.IssuerCount}}} issuers. This indicates that requests for each issuer are being sent to this instance of IdentityServer, which may be due to a network infrastructure configuration issue. If you intend to use multiple issuers, please contact {{{LicenseLogParameters.LicenseContact}}} from {{{LicenseLogParameters.LicenseCompany}}} or start a conversation with us at https://duende.link/l/contact to upgrade your license as soon as possible. In a future version, this limit will be enforced after a threshold is exceeded. The issuers used were {{{LicenseLogParameters.IssuersUsed}}}. See https://duende.link/l/threshold for more information.")] + public static partial void IssuerLimitExceededOverThreshold(this ILogger logger, + int issuerLimit, int issuerCount, string licenseContact, string licenseCompany, IReadOnlyCollection issuersUsed); + [LoggerMessage( LogLevel.Error, - message: $"Your license for Duende IdentityServer only permits {{{LicenseLogParameters.IssuerLimit}}} number of issuers. You have processed requests for {{{LicenseLogParameters.IssuerCount}}} issuers and are over the threshold of {{{LicenseLogParameters.IssuerLimitExceededThreshold}}}. The issuers used were: {{{LicenseLogParameters.IssuersUsed}}}. This might be due to your server being accessed via different URLs or a direct IP and/or you have reverse proxy or a gateway involved. This suggests a network infrastructure configuration problem, or you are deliberately hosting multiple URLs and require an upgraded license. In a future version of issuer limit will be enforced.")] - public static partial void IssuerLimitExceededOverThreshold(this ILogger logger, int issuerLimit, int issuerCount, int issuerLimitExceededThreshold, IReadOnlyCollection issuersUsed); - - [LoggerMessage( - LogLevel.Error, - message: $"You do not have a license, and you have processed requests for {{{LicenseLogParameters.IssuerCount}}} issuers. If you are deliberately hosting multiple URLs then this number requires a license per issuer, or the Enterprise Edition tier of license. If not then this might be due to your server being accessed via different URLs or a direct IP and/or you have reverse proxy or a gateway involved, and this suggests a network infrastructure configuration problem. The issuers used were: {{{LicenseLogParameters.IssuersUsed}}}.")] + message: $"You are using IdentityServer in trial mode and have processed requests for {{{LicenseLogParameters.IssuerCount}}} issuers. This indicates that requests for each issuer are being sent to this instance of IdentityServer, which may be due to a network infrastructure configuration issue. If you intend to use multiple issuers, either a license per issuer or an Enterprise Edition license is required. In a future version, this limit will be enforced after a threshold is exceeded. The issuers used were: {{{LicenseLogParameters.IssuersUsed}}}. See https://duende.link/l/trial for more information.")] public static partial void IssuerLimitWithNoLicenseExceeded(this ILogger logger, int issuerCount, IReadOnlyCollection issuersUsed); } diff --git a/identity-server/src/IdentityServer/Services/Default/DefaultSessionCoordinationService.cs b/identity-server/src/IdentityServer/Services/Default/DefaultSessionCoordinationService.cs index 78cc9596e..33820f4b3 100644 --- a/identity-server/src/IdentityServer/Services/Default/DefaultSessionCoordinationService.cs +++ b/identity-server/src/IdentityServer/Services/Default/DefaultSessionCoordinationService.cs @@ -140,14 +140,17 @@ public class DefaultSessionCoordinationService : ISessionCoordinationService { var client = await ClientStore.FindClientByIdAsync(clientId); // i don't think we care if it's an enabled client at this point - var shouldCoordinate = - client.CoordinateLifetimeWithUserSession == true || - (Options.Authentication.CoordinateClientLifetimesWithUserSession && client.CoordinateLifetimeWithUserSession != false); - - if (shouldCoordinate) + if (client != null) { - // this implies they should also be contacted for backchannel logout below - clientsToCoordinate.Add(clientId); + var shouldCoordinate = + client.CoordinateLifetimeWithUserSession == true || + (Options.Authentication.CoordinateClientLifetimesWithUserSession && client.CoordinateLifetimeWithUserSession != false); + + if (shouldCoordinate) + { + // this implies they should also be contacted for backchannel logout below + clientsToCoordinate.Add(clientId); + } } } diff --git a/identity-server/src/Storage/Models/GrantType.cs b/identity-server/src/Storage/Models/GrantType.cs index b67ec0d17..48e96fbbc 100644 --- a/identity-server/src/Storage/Models/GrantType.cs +++ b/identity-server/src/Storage/Models/GrantType.cs @@ -14,4 +14,5 @@ public static class GrantType public const string ClientCredentials = "client_credentials"; public const string ResourceOwnerPassword = "password"; public const string DeviceFlow = "urn:ietf:params:oauth:grant-type:device_code"; + public const string RefreshToken = "refresh_token"; } diff --git a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Authorize/PushedAuthorizationTests.cs b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Authorize/PushedAuthorizationTests.cs index 3af1dc856..961a38fcb 100644 --- a/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Authorize/PushedAuthorizationTests.cs +++ b/identity-server/test/IdentityServer.IntegrationTests/Endpoints/Authorize/PushedAuthorizationTests.cs @@ -21,6 +21,7 @@ public class PushedAuthorizationTests { private readonly IdentityServerPipeline _mockPipeline = new(); private Client _client; + private string clientSecret = Guid.NewGuid().ToString(); private Client _client2; private WilsonJsonWebKey _privateKey; @@ -33,7 +34,7 @@ public class PushedAuthorizationTests ConfigureUsers(); ConfigureScopesAndResources(); - _mockPipeline.Initialize(); + _mockPipeline.Initialize(enableLogging: true); _mockPipeline.Options.Endpoints.EnablePushedAuthorizationEndpoint = true; } @@ -100,12 +101,32 @@ public class PushedAuthorizationTests authorization.State.ShouldBe(expectedState); } + [Fact] + public async Task sensitive_values_should_not_be_logged_on_bad_request_to_par_endpoint() + { + // Login + await _mockPipeline.LoginAsync("bob"); + _mockPipeline.BrowserClient.AllowAutoRedirect = false; + + // Push Authorization + var expectedCallback = _client.RedirectUris.First(); + var expectedState = "123_state"; + var (parJson, statusCode) = await _mockPipeline.PushAuthorizationRequestAsync( + clientSecret: clientSecret, + redirectUri: "bogus", // <-- Intentionally wrong, to provoke logging an error with raw request + state: expectedState + ); + + _mockPipeline.MockLogger.LogMessages.ShouldContain(msg => msg.Contains("\"client_secret\": \"***REDACTED***\"")); + _mockPipeline.MockLogger.LogMessages.ShouldNotContain(msg => msg.Contains(clientSecret)); + } + [Fact] public async Task using_pushed_authorization_when_it_is_globally_disabled_fails() { _mockPipeline.Options.Endpoints.EnablePushedAuthorizationEndpoint = false; - var (_, statusCode) = await _mockPipeline.PushAuthorizationRequestAsync(); + var (_, statusCode) = await _mockPipeline.PushAuthorizationRequestAsync(clientSecret: clientSecret); statusCode.ShouldBe(HttpStatusCode.NotFound); } @@ -155,7 +176,7 @@ public class PushedAuthorizationTests public async Task existing_pushed_authorization_request_uris_become_invalid_when_par_is_disabled() { // PAR is enabled when we push authorization... - var (parJson, statusCode) = await _mockPipeline.PushAuthorizationRequestAsync(); + var (parJson, statusCode) = await _mockPipeline.PushAuthorizationRequestAsync(clientSecret: clientSecret); statusCode.ShouldBe(HttpStatusCode.Created); parJson.ShouldNotBeNull(); @@ -183,7 +204,7 @@ public class PushedAuthorizationTests // Login await _mockPipeline.LoginAsync("bob"); - var (parJson, statusCode) = await _mockPipeline.PushAuthorizationRequestAsync(); + var (parJson, statusCode) = await _mockPipeline.PushAuthorizationRequestAsync(clientSecret: clientSecret); ; statusCode.ShouldBe(HttpStatusCode.Created); parJson.ShouldNotBeNull(); @@ -351,7 +372,7 @@ public class PushedAuthorizationTests ClientId = "client1", ClientSecrets = new [] { - new Secret("secret".Sha256()) + new Secret(clientSecret.Sha256()) }, AllowedGrantTypes = GrantTypes.Implicit, RequireConsent = false, diff --git a/identity-server/test/IdentityServer.IntegrationTests/Hosting/LicenseTests.cs b/identity-server/test/IdentityServer.IntegrationTests/Hosting/LicenseTests.cs index c6f9fb149..776685531 100644 --- a/identity-server/test/IdentityServer.IntegrationTests/Hosting/LicenseTests.cs +++ b/identity-server/test/IdentityServer.IntegrationTests/Hosting/LicenseTests.cs @@ -72,6 +72,6 @@ public class LicenseTests : IDisposable } _mockPipeline.MockLogger.LogMessages.ShouldContain( - $"You are using IdentityServer in trial mode and have exceeded the trial threshold of {threshold} requests handled by IdentityServer. In a future version, you will need to restart the server or configure a license key to continue testing. For more information, please see http://duende.link/trialmode."); + $"You are using IdentityServer in trial mode and have exceeded the trial threshold of {threshold} requests handled by IdentityServer. In a future version, you will need to restart the server or configure a license key to continue testing. See https://duende.link/l/trial for more information."); } } diff --git a/identity-server/test/IdentityServer.UnitTests/Endpoints/Results/ProtectedResourceErrorResultTests.cs b/identity-server/test/IdentityServer.UnitTests/Endpoints/Results/ProtectedResourceErrorResultTests.cs new file mode 100644 index 000000000..9cff25ee7 --- /dev/null +++ b/identity-server/test/IdentityServer.UnitTests/Endpoints/Results/ProtectedResourceErrorResultTests.cs @@ -0,0 +1,61 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + +using Duende.IdentityServer.Endpoints.Results; +using Microsoft.AspNetCore.Http; +using Microsoft.Net.Http.Headers; + +namespace UnitTests.Endpoints.Results; + +public class ProtectedResourceErrorResultTests +{ + private readonly ProtectedResourceErrorHttpWriter writer = new(); + + [Fact] + public void WwwAuthenticate_header_with_error_and_description_should_be_a_single_line() + { + var context = new DefaultHttpContext(); + + writer.WriteHttpResponse( + new ProtectedResourceErrorResult("oops", "big oops"), + context + ); + + var wwwAuthHeader = context.Response.Headers[HeaderNames.WWWAuthenticate].ToString(); + wwwAuthHeader.ShouldBe( + """ + Bearer realm="IdentityServer",error="oops",error_description="big oops" + """); + } + + [Fact] + public void WwwAuthenticate_header_with_error_should_be_a_single_line() + { + var context = new DefaultHttpContext(); + + writer.WriteHttpResponse( + new ProtectedResourceErrorResult("oops"), + context + ); + + var wwwAuthHeader = context.Response.Headers[HeaderNames.WWWAuthenticate].ToString(); + wwwAuthHeader.ShouldBe( + """ + Bearer realm="IdentityServer",error="oops" + """); + } + + [Fact] + public void WwwAuthenticate_header_should_always_be_a_single_string_value() + { + var context = new DefaultHttpContext(); + + writer.WriteHttpResponse( + new ProtectedResourceErrorResult("oops", "big oops"), + context + ); + + var wwwAuthHeader = context.Response.Headers[HeaderNames.WWWAuthenticate]; + wwwAuthHeader.Count.ShouldBe(1); + } +} diff --git a/identity-server/test/IdentityServer.UnitTests/Hosting/MutualTlsEndpointMiddlewareTests.cs b/identity-server/test/IdentityServer.UnitTests/Hosting/MutualTlsEndpointMiddlewareTests.cs index bc32c2942..3d21fd54f 100644 --- a/identity-server/test/IdentityServer.UnitTests/Hosting/MutualTlsEndpointMiddlewareTests.cs +++ b/identity-server/test/IdentityServer.UnitTests/Hosting/MutualTlsEndpointMiddlewareTests.cs @@ -382,6 +382,149 @@ public class MutualTlsEndpointMiddlewareTests context.Request.Path.ToString().ShouldStartWith("/"); } + [Fact] + internal void mtls_endpoint_type_when_mtls_disabled_should_be_none() + { + _options.MutualTls.Enabled = false; + var context = CreateContext(); + var result = _subject.DetermineMtlsEndpointType(context, out var subPath); + result.ShouldBe(MutualTlsEndpointMiddleware.MtlsEndpointType.None); + subPath.ShouldBeNull(); + } + + [Theory] + [InlineData("mtls.example.com", "mtls.example.com", MutualTlsEndpointMiddleware.MtlsEndpointType.SeparateDomain)] + [InlineData("mTLS.example.com", "mtls.example.com", MutualTlsEndpointMiddleware.MtlsEndpointType.SeparateDomain)] + [InlineData("mtls.example.com", "mTLS.example.com", MutualTlsEndpointMiddleware.MtlsEndpointType.SeparateDomain)] + [InlineData("mtls.example.com:443", "mtls.example.com", MutualTlsEndpointMiddleware.MtlsEndpointType.SeparateDomain)] + [InlineData("mtls.example.com:5001", "mtls.example.com", MutualTlsEndpointMiddleware.MtlsEndpointType.None)] + [InlineData("mtls.example.com", "mtls.example.com:443", MutualTlsEndpointMiddleware.MtlsEndpointType.SeparateDomain)] + [InlineData("mtls.example.com:443", "mtls.example.com:443", MutualTlsEndpointMiddleware.MtlsEndpointType.SeparateDomain)] + [InlineData("mtls.example.com:5001", "mtls.example.com:443", MutualTlsEndpointMiddleware.MtlsEndpointType.None)] + [InlineData("mtls.example.com", "mtls.example.com:5001", MutualTlsEndpointMiddleware.MtlsEndpointType.None)] + [InlineData("mtls.example.com:443", "mtls.example.com:5001", MutualTlsEndpointMiddleware.MtlsEndpointType.None)] + [InlineData("mtls.example.com:5001", "mtls.example.com:5001", MutualTlsEndpointMiddleware.MtlsEndpointType.SeparateDomain)] + internal void mtls_endpoint_type_separate_domain_should_be_detected(string requestedHost, string configuredDomainName, MutualTlsEndpointMiddleware.MtlsEndpointType expectedType) + { + // Arrange + _options.MutualTls.Enabled = true; + _options.MutualTls.DomainName = configuredDomainName; + var context = CreateContext(); + context.Request.Host = new HostString(requestedHost); + + // Act + var result = _subject.DetermineMtlsEndpointType(context, out var subPath); + + // Assert + result.ShouldBe(expectedType); + subPath.ShouldBeNull(); + } + + [Theory] + [InlineData("example.com", "mtls.example.com")] + [InlineData("example.com:443", "mtls.example.com")] + [InlineData("example.com:5001", "mtls.example.com")] + [InlineData("other.example.com", "mtls.example.com")] + [InlineData("other.example.com:443", "mtls.example.com")] + [InlineData("example.com", "mtls.example.com:443")] + [InlineData("example.com:443", "mtls.example.com:443")] + [InlineData("other.example.com", "mtls.example.com:5001")] + [InlineData("other.example.com:5001", "mtls.example.com:5001")] + internal void mtls_endpoint_type_separate_domain_should_not_match_different_domain(string requestedHost, string configuredDomainName) + { + // Arrange + _options.MutualTls.Enabled = true; + _options.MutualTls.DomainName = configuredDomainName; + var context = CreateContext(); + context.Request.Host = new HostString(requestedHost); + + // Act + var result = _subject.DetermineMtlsEndpointType(context, out var subPath); + + // Assert + result.ShouldBe(MutualTlsEndpointMiddleware.MtlsEndpointType.None); + subPath.ShouldBeNull(); + } + + [Theory] + [InlineData("mtls.example.com", "mtls")] + [InlineData("mtls.example.com", "mTLS")] + [InlineData("mTLS.example.com", "mtls")] + [InlineData("mtls.example.com:443", "mtls")] + [InlineData("mtls.example.com:5001", "mtls")] + internal void mtls_endpoint_type_subdomain_should_be_detected(string requestedHost, string configuredDomainName) + { + // Arrange + _options.MutualTls.Enabled = true; + _options.MutualTls.DomainName = configuredDomainName; + var context = CreateContext(); + context.Request.Host = new HostString(requestedHost); + + // Act + var result = _subject.DetermineMtlsEndpointType(context, out var subPath); + + // Assert + result.ShouldBe(MutualTlsEndpointMiddleware.MtlsEndpointType.Subdomain); + subPath.ShouldBeNull(); + } + + [Theory] + [InlineData("api.example.com", "mtls")] + [InlineData("api.example.com:443", "mtls")] + [InlineData("example.com", "mtls")] + [InlineData("example.com:5001", "mtls")] + internal void mtls_endpoint_type_subdomain_should_not_match_different_subdomain(string requestedHost, string configuredDomainName) + { + // Arrange + _options.MutualTls.Enabled = true; + _options.MutualTls.DomainName = configuredDomainName; + var context = CreateContext(); + context.Request.Host = new HostString(requestedHost); + + // Act + var result = _subject.DetermineMtlsEndpointType(context, out var subPath); + + // Assert + result.ShouldBe(MutualTlsEndpointMiddleware.MtlsEndpointType.None); + subPath.ShouldBeNull(); + } + + [Theory] + [InlineData("/connect/mtls/token")] + [InlineData("/connect/mTLS/token")] + internal void mtls_endpoint_type_path_based_should_be_detected(string requestedPath) + { + // Arrange + _options.MutualTls.Enabled = true; + var context = CreateContext(); + context.Request.Path = new PathString(requestedPath); + + // Act + var result = _subject.DetermineMtlsEndpointType(context, out var subPath); + + // Assert + result.ShouldBe(MutualTlsEndpointMiddleware.MtlsEndpointType.PathBased); + subPath.Value.ToString().ShouldBe("/token"); + } + + [Fact] + internal void mtls_endpoint_type_should_be_none_when_enabled_but_no_matching_configuration() + { + // Arrange + _options.MutualTls.Enabled = true; + _options.MutualTls.DomainName = "mtls.example.com"; + var context = CreateContext(); + context.Request.Host = new HostString("regular.example.com"); + context.Request.Path = new PathString("/connect/token"); + + // Act + var result = _subject.DetermineMtlsEndpointType(context, out var subPath); + + // Assert + result.ShouldBe(MutualTlsEndpointMiddleware.MtlsEndpointType.None); + subPath.ShouldBeNull(); + } + private async Task GetResponseBodyAsString(HttpContext context) { context.Response.Body.Seek(0, SeekOrigin.Begin); diff --git a/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticEntries/BasicServerInfoDiagnosticEntryTests.cs b/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticEntries/BasicServerInfoDiagnosticEntryTests.cs index 46eebaeda..b9f464ea8 100644 --- a/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticEntries/BasicServerInfoDiagnosticEntryTests.cs +++ b/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticEntries/BasicServerInfoDiagnosticEntryTests.cs @@ -12,11 +12,15 @@ public class BasicServerInfoDiagnosticEntryTests public async Task WriteAsync_ShouldWriteBasicServerInfo() { const string expectedHostName = "testing.local"; + var expectedServerStartTime = DateTime.UtcNow.AddMinutes(-5); + var expectedCurrentServerTime = DateTime.UtcNow; var subject = new BasicServerInfoDiagnosticEntry(() => expectedHostName); - var result = await DiagnosticEntryTestHelper.WriteEntryToJson(subject); + var result = await DiagnosticEntryTestHelper.WriteEntryToJson(subject, expectedServerStartTime, expectedCurrentServerTime); var basicServerInfo = result.RootElement.GetProperty("BasicServerInfo"); basicServerInfo.GetProperty("HostName").GetString().ShouldBe(expectedHostName); + basicServerInfo.GetProperty("ServerStartTime").GetString().ShouldBe(expectedServerStartTime.ToString("o")); + basicServerInfo.GetProperty("CurrentServerTime").GetString().ShouldBe(expectedCurrentServerTime.ToString("o")); } } diff --git a/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticEntries/DiagnosticEntryTestHelper.cs b/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticEntries/DiagnosticEntryTestHelper.cs index 0c6a74f06..9147a1bc5 100644 --- a/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticEntries/DiagnosticEntryTestHelper.cs +++ b/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticEntries/DiagnosticEntryTestHelper.cs @@ -10,14 +10,14 @@ namespace IdentityServer.UnitTests.Licensing.V2.DiagnosticEntries; internal static class DiagnosticEntryTestHelper { - public static async Task WriteEntryToJson(IDiagnosticEntry subject) + public static async Task WriteEntryToJson(IDiagnosticEntry subject, DateTime? serverStartTime = null, DateTime? currentServerTime = null) { var bufferWriter = new ArrayBufferWriter(); await using var writer = new Utf8JsonWriter(bufferWriter, new JsonWriterOptions { Indented = false }); writer.WriteStartObject(); - await subject.WriteAsync(writer); + await subject.WriteAsync(new DiagnosticContext(serverStartTime ?? DateTime.UtcNow.AddMinutes(-5), currentServerTime ?? DateTime.UtcNow), writer); writer.WriteEndObject(); await writer.FlushAsync(); diff --git a/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticEntries/TokenIssueCountDiagnosticEntryTests.cs b/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticEntries/TokenIssueCountDiagnosticEntryTests.cs index 76dc13cf2..e1040e1ce 100644 --- a/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticEntries/TokenIssueCountDiagnosticEntryTests.cs +++ b/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticEntries/TokenIssueCountDiagnosticEntryTests.cs @@ -18,7 +18,7 @@ public class TokenIssueCountDiagnosticEntryTests var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); - result.RootElement.GetProperty("TokenIssueCounts").GetProperty("Jwt").GetInt64().ShouldBe(1); + result.RootElement.GetProperty("TokenIssueCounts").GetProperty("AccessTokensByType").GetProperty("Jwt").GetInt64().ShouldBe(1); } [Fact] @@ -28,67 +28,67 @@ public class TokenIssueCountDiagnosticEntryTests var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); - result.RootElement.GetProperty("TokenIssueCounts").GetProperty("Reference").GetInt64().ShouldBe(1); + result.RootElement.GetProperty("TokenIssueCounts").GetProperty("AccessTokensByType").GetProperty("Reference").GetInt64().ShouldBe(1); } [Fact] - public async Task Should_Count_JwtDPoPToken() + public async Task Should_Count_DPoP_Constraint_For_DPoP_Constrained_JWT() { IssueToken(GrantType.AuthorizationCode, true, AccessTokenType.Jwt, false, ProofType.DPoP, false); var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); - result.RootElement.GetProperty("TokenIssueCounts").GetProperty("JwtDPoP").GetInt64().ShouldBe(1); + result.RootElement.GetProperty("TokenIssueCounts").GetProperty("AccessTokensBySenderConstraint").GetProperty("DPoP").GetInt64().ShouldBe(1); } [Fact] - public async Task Should_Count_ReferenceDPoPToken() + public async Task Should_Count_DPoP_Constraint_For_DPoP_Constrained_Reference_Token() { IssueToken(GrantType.AuthorizationCode, true, AccessTokenType.Reference, false, ProofType.DPoP, false); var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); - result.RootElement.GetProperty("TokenIssueCounts").GetProperty("ReferenceDPoP").GetInt64().ShouldBe(1); + result.RootElement.GetProperty("TokenIssueCounts").GetProperty("AccessTokensBySenderConstraint").GetProperty("DPoP").GetInt64().ShouldBe(1); } [Fact] - public async Task Should_Count_JwtMTlsToken() + public async Task Should_Count_mTLS_Constraint_For_mTLS_Constrained_JWT() { IssueToken(GrantType.AuthorizationCode, true, AccessTokenType.Jwt, false, ProofType.ClientCertificate, false); var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); - result.RootElement.GetProperty("TokenIssueCounts").GetProperty("JwtMTLS").GetInt64().ShouldBe(1); + result.RootElement.GetProperty("TokenIssueCounts").GetProperty("AccessTokensBySenderConstraint").GetProperty("mTLS").GetInt64().ShouldBe(1); } [Fact] - public async Task Should_Count_ReferenceMTlsToken() + public async Task Should_Count_mTLS_Constraint_For_mTLS_Constrained_Reference_Token() { IssueToken(GrantType.AuthorizationCode, true, AccessTokenType.Reference, false, ProofType.ClientCertificate, false); var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); - result.RootElement.GetProperty("TokenIssueCounts").GetProperty("ReferenceMTLS").GetInt64().ShouldBe(1); + result.RootElement.GetProperty("TokenIssueCounts").GetProperty("AccessTokensBySenderConstraint").GetProperty("mTLS").GetInt64().ShouldBe(1); } [Fact] - public async Task Should_Count_RefreshToken() + public async Task Should_Count_Refresh_Token() { IssueToken("refresh_token", false, AccessTokenType.Jwt, true, ProofType.None, false); var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); - result.RootElement.GetProperty("TokenIssueCounts").GetProperty("Refresh").GetInt64().ShouldBe(1); + result.RootElement.GetProperty("TokenIssueCounts").GetProperty("TokensByType").GetProperty("Refresh").GetInt64().ShouldBe(1); } [Fact] - public async Task Should_Count_IdToken() + public async Task Should_Count_Id_Token() { IssueToken(GrantType.AuthorizationCode, false, AccessTokenType.Jwt, false, ProofType.None, true); var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); - result.RootElement.GetProperty("TokenIssueCounts").GetProperty("Id").GetInt64().ShouldBe(1); + result.RootElement.GetProperty("TokenIssueCounts").GetProperty("TokensByType").GetProperty("Id").GetInt64().ShouldBe(1); } [Fact] @@ -100,9 +100,11 @@ public class TokenIssueCountDiagnosticEntryTests var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); var tokenIssueCounts = result.RootElement.GetProperty("TokenIssueCounts"); - tokenIssueCounts.GetProperty("Jwt").GetInt64().ShouldBe(1); - tokenIssueCounts.GetProperty("JwtDPoP").GetInt64().ShouldBe(1); - tokenIssueCounts.GetProperty("Refresh").GetInt64().ShouldBe(1); + tokenIssueCounts.GetProperty("AccessTokensByType").GetProperty("Jwt").GetInt64().ShouldBe(2); + var senderConstraint = tokenIssueCounts.GetProperty("AccessTokensBySenderConstraint"); + senderConstraint.GetProperty("None").GetInt64().ShouldBe(1); + senderConstraint.GetProperty("DPoP").GetInt64().ShouldBe(1); + tokenIssueCounts.GetProperty("TokensByType").GetProperty("Refresh").GetInt64().ShouldBe(1); } [Fact] @@ -113,14 +115,17 @@ public class TokenIssueCountDiagnosticEntryTests var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); var tokenIssueCounts = result.RootElement.GetProperty("TokenIssueCounts"); - tokenIssueCounts.GetProperty("Jwt").GetInt64().ShouldBe(0); - tokenIssueCounts.GetProperty("Reference").GetInt64().ShouldBe(0); - tokenIssueCounts.GetProperty("JwtDPoP").GetInt64().ShouldBe(0); - tokenIssueCounts.GetProperty("JwtMTLS").GetInt64().ShouldBe(0); - tokenIssueCounts.GetProperty("ReferenceDPoP").GetInt64().ShouldBe(0); - tokenIssueCounts.GetProperty("ReferenceMTLS").GetInt64().ShouldBe(0); - tokenIssueCounts.GetProperty("Refresh").GetInt64().ShouldBe(0); - tokenIssueCounts.GetProperty("Id").GetInt64().ShouldBe(0); + var accessTokensByType = tokenIssueCounts.GetProperty("AccessTokensByType"); + accessTokensByType.GetProperty("Jwt").GetInt64().ShouldBe(0); + accessTokensByType.GetProperty("Reference").GetInt64().ShouldBe(0); + var senderConstraint = tokenIssueCounts.GetProperty("AccessTokensBySenderConstraint"); + senderConstraint.GetProperty("None").GetInt64().ShouldBe(0); + senderConstraint.GetProperty("DPoP").GetInt64().ShouldBe(0); + senderConstraint.GetProperty("mTLS").GetInt64().ShouldBe(0); + var tokensByType = tokenIssueCounts.GetProperty("TokensByType"); + tokensByType.GetProperty("Access").GetInt64().ShouldBe(0); + tokensByType.GetProperty("Refresh").GetInt64().ShouldBe(0); + tokensByType.GetProperty("Id").GetInt64().ShouldBe(0); } [Fact] @@ -131,7 +136,7 @@ public class TokenIssueCountDiagnosticEntryTests var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); var tokenIssueCounts = result.RootElement.GetProperty("TokenIssueCounts"); - tokenIssueCounts.GetProperty(GrantType.AuthorizationCode).GetInt64().ShouldBe(1); + tokenIssueCounts.GetProperty("RequestsByGrantType").GetProperty(GrantType.AuthorizationCode).GetInt64().ShouldBe(1); } [Fact] @@ -143,8 +148,9 @@ public class TokenIssueCountDiagnosticEntryTests var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); var tokenIssueCounts = result.RootElement.GetProperty("TokenIssueCounts"); - tokenIssueCounts.GetProperty(GrantType.AuthorizationCode).GetInt64().ShouldBe(1); - tokenIssueCounts.GetProperty(GrantType.ClientCredentials).GetInt64().ShouldBe(1); + var grantTypeCounts = tokenIssueCounts.GetProperty("RequestsByGrantType"); + grantTypeCounts.GetProperty(GrantType.AuthorizationCode).GetInt64().ShouldBe(1); + grantTypeCounts.GetProperty(GrantType.ClientCredentials).GetInt64().ShouldBe(1); } [Fact] @@ -156,7 +162,7 @@ public class TokenIssueCountDiagnosticEntryTests var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); var tokenIssueCounts = result.RootElement.GetProperty("TokenIssueCounts"); - tokenIssueCounts.GetProperty(GrantType.AuthorizationCode).GetInt64().ShouldBe(2); + tokenIssueCounts.GetProperty("RequestsByGrantType").GetProperty(GrantType.AuthorizationCode).GetInt64().ShouldBe(2); } [Fact] @@ -174,9 +180,10 @@ public class TokenIssueCountDiagnosticEntryTests var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); var tokenIssueCounts = result.RootElement.GetProperty("TokenIssueCounts"); + var grantTypeCounts = tokenIssueCounts.GetProperty("RequestsByGrantType"); foreach (var grantType in grantTypes) { - tokenIssueCounts.GetProperty(grantType).GetInt64().ShouldBe(1); + grantTypeCounts.GetProperty(grantType).GetInt64().ShouldBe(1); } } @@ -188,13 +195,17 @@ public class TokenIssueCountDiagnosticEntryTests var result = await DiagnosticEntryTestHelper.WriteEntryToJson(_subject); var tokenIssueCounts = result.RootElement.GetProperty("TokenIssueCounts"); - tokenIssueCounts.GetProperty("Jwt").GetInt64().ShouldBe(0); - tokenIssueCounts.GetProperty("Reference").GetInt64().ShouldBe(0); - tokenIssueCounts.GetProperty("JwtDPoP").GetInt64().ShouldBe(0); - tokenIssueCounts.GetProperty("JwtMTLS").GetInt64().ShouldBe(0); - tokenIssueCounts.GetProperty("ReferenceDPoP").GetInt64().ShouldBe(0); - tokenIssueCounts.GetProperty("ReferenceMTLS").GetInt64().ShouldBe(0); - tokenIssueCounts.GetProperty("Refresh").GetInt64().ShouldBe(0); + var accessTokensByType = tokenIssueCounts.GetProperty("AccessTokensByType"); + accessTokensByType.GetProperty("Jwt").GetInt64().ShouldBe(0); + accessTokensByType.GetProperty("Reference").GetInt64().ShouldBe(0); + var senderConstraint = tokenIssueCounts.GetProperty("AccessTokensBySenderConstraint"); + senderConstraint.GetProperty("None").GetInt64().ShouldBe(0); + senderConstraint.GetProperty("DPoP").GetInt64().ShouldBe(0); + senderConstraint.GetProperty("mTLS").GetInt64().ShouldBe(0); + var tokensByType = tokenIssueCounts.GetProperty("TokensByType"); + tokensByType.GetProperty("Access").GetInt64().ShouldBe(0); + tokensByType.GetProperty("Refresh").GetInt64().ShouldBe(0); + tokensByType.GetProperty("Id").GetInt64().ShouldBe(0); } private void IssueToken(string grantType, bool accessTokenIssued, AccessTokenType? accessTokenType, bool refreshTokenIssued, diff --git a/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticSummaryTests.cs b/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticSummaryTests.cs index 00a334ebd..ab3ae75bd 100644 --- a/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticSummaryTests.cs +++ b/identity-server/test/IdentityServer.UnitTests/Licensing/v2/DiagnosticSummaryTests.cs @@ -25,7 +25,7 @@ public class DiagnosticSummaryTests secondDiagnosticEntry, thirdDiagnosticEntry }; - var summary = new DiagnosticSummary(entries, new IdentityServerOptions(), new StubLoggerFactory(logger)); + var summary = new DiagnosticSummary(DateTime.UtcNow, entries, new IdentityServerOptions(), new StubLoggerFactory(logger)); await summary.PrintSummary(); @@ -42,7 +42,7 @@ public class DiagnosticSummaryTests var logger = new FakeLogger(); var diagnosticEntry = new LongDiagnosticEntry { OutputLength = chunkSize * 2 }; - var summary = new DiagnosticSummary([diagnosticEntry], options, new StubLoggerFactory(logger)); + var summary = new DiagnosticSummary(DateTime.UtcNow, [diagnosticEntry], options, new StubLoggerFactory(logger)); await summary.PrintSummary(); @@ -61,7 +61,7 @@ public class DiagnosticSummaryTests var logger = new FakeLogger(); var diagnosticEntry = new LongDiagnosticEntry { OutputLength = 2, OutputCharacter = '€' }; - var summary = new DiagnosticSummary([diagnosticEntry], options, new StubLoggerFactory(logger)); + var summary = new DiagnosticSummary(DateTime.UtcNow, [diagnosticEntry], options, new StubLoggerFactory(logger)); await summary.PrintSummary(); @@ -76,7 +76,7 @@ public class DiagnosticSummaryTests var logger = new FakeLogger(); var diagnosticEntry = new LongDiagnosticEntry { OutputLength = options.Diagnostics.ChunkSize * 2 }; - var summary = new DiagnosticSummary([diagnosticEntry], options, new StubLoggerFactory(logger)); + var summary = new DiagnosticSummary(DateTime.UtcNow, [diagnosticEntry], options, new StubLoggerFactory(logger)); await summary.PrintSummary(); foreach (var entry in logger.Collector.GetSnapshot()) @@ -91,7 +91,7 @@ public class DiagnosticSummaryTests var options = new IdentityServerOptions(); var logger = new FakeLogger(); var diagnosticEntry = new LongDiagnosticEntry { OutputLength = 100000 }; - var summary = new DiagnosticSummary([diagnosticEntry], options, new StubLoggerFactory(logger)); + var summary = new DiagnosticSummary(DateTime.UtcNow, [diagnosticEntry], options, new StubLoggerFactory(logger)); await summary.PrintSummary(); @@ -103,7 +103,7 @@ public class DiagnosticSummaryTests private class TestDiagnosticEntry : IDiagnosticEntry { public bool WasCalled { get; private set; } - public Task WriteAsync(Utf8JsonWriter writer) + public Task WriteAsync(DiagnosticContext context, Utf8JsonWriter writer) { WasCalled = true; return Task.CompletedTask; @@ -115,7 +115,7 @@ public class DiagnosticSummaryTests public int OutputLength { get; set; } public char OutputCharacter { get; set; } = 'x'; - public Task WriteAsync(Utf8JsonWriter writer) + public Task WriteAsync(DiagnosticContext context, Utf8JsonWriter writer) { writer.WriteString("test", new string(OutputCharacter, OutputLength)); return Task.CompletedTask; diff --git a/identity-server/test/IdentityServer.UnitTests/Licensing/v2/LicenseExpirationCheckerTests.cs b/identity-server/test/IdentityServer.UnitTests/Licensing/v2/LicenseExpirationCheckerTests.cs index 582cb037b..c611ee64a 100644 --- a/identity-server/test/IdentityServer.UnitTests/Licensing/v2/LicenseExpirationCheckerTests.cs +++ b/identity-server/test/IdentityServer.UnitTests/Licensing/v2/LicenseExpirationCheckerTests.cs @@ -44,7 +44,7 @@ public class LicenseExpirationCheckerTests // REMINDER - If this test needs to change because the log message was updated, so should no_warning_is_logged_for_unexpired_license _logger.Collector.GetSnapshot().ShouldContain(r => r.Message == - "The IdentityServer license is expired. In a future version of IdentityServer, license expiration will be enforced after a grace period.", + "Your IdentityServer license is expired. Please contact joe@duendesoftware.com from _test or start a conversation with us at https://duende.link/l/contact to renew your license as soon as possible. In a future version, license expiration will be enforced after a grace period. See https://duende.link/l/expired for more information.", 1); } @@ -58,7 +58,7 @@ public class LicenseExpirationCheckerTests _expirationCheck.CheckExpiration(); _logger.Collector.GetSnapshot().ShouldNotContain(r => - r.Message == "The IdentityServer license is expired. In a future version of IdentityServer, license expiration will be enforced after a grace period."); + r.Message == "Your IdentityServer license is expired. Please contact joe@duendesoftware.com from _test or start a conversation with us at https://duende.link/l/contact to renew your license as soon as possible. In a future version, license expiration will be enforced after a grace period. See https://duende.link/l/expired for more information."); } [Theory] diff --git a/identity-server/test/IdentityServer.UnitTests/Licensing/v2/LicenseUsageTests.cs b/identity-server/test/IdentityServer.UnitTests/Licensing/v2/LicenseUsageTests.cs index 9bd231542..22dedd3af 100644 --- a/identity-server/test/IdentityServer.UnitTests/Licensing/v2/LicenseUsageTests.cs +++ b/identity-server/test/IdentityServer.UnitTests/Licensing/v2/LicenseUsageTests.cs @@ -99,8 +99,8 @@ public class LicenseUsageTests var initialLogSnapshot = _logger.Collector.GetSnapshot(); initialLogSnapshot.ShouldContain(r => r.Level == LogLevel.Error && - r.Message.StartsWith( - "You do not have a license, and you have processed requests for 6 clients. This number requires a tier of license higher than Starter Edition. The clients used were:")); + r.Message == + "You are using IdentityServer in trial mode and have processed requests for 6 clients. In production, this will require a license with sufficient client capacity. You can either purchase a license tier that includes this many clients or add additional client capacity to a Starter Edition license. The clients used were: client3, client2, client1, client0, client5, client4. See https://duende.link/l/trial for more information."); } [Fact] @@ -116,8 +116,7 @@ public class LicenseUsageTests var logSnapshot = _logger.Collector.GetSnapshot(); logSnapshot.ShouldContain(r => r.Level == LogLevel.Error && - r.Message.StartsWith( - "Your license for Duende IdentityServer only permits 5 number of clients. You have processed requests for 6 clients and are still within the threshold of 5 for exceeding permitted clients. In a future version of client limit will be enforced. The clients used were:")); + r.Message == "Your IdentityServer license includes 5 clients but you have processed requests for 6 clients. Please contact joe@duendesoftware.com from _test or start a conversation with us at https://duende.link/l/contact to upgrade your license as soon as possible. In a future version, this limit will be enforced after a threshold is exceeded. The clients used were: client3, client2, client1, client0, client5, client4. See https://duende.link/l/threshold for more information."); } [Fact] @@ -148,7 +147,7 @@ public class LicenseUsageTests var logSnapshot = _logger.Collector.GetSnapshot(); logSnapshot.ShouldContain(r => r.Level == LogLevel.Error && - r.Message.StartsWith("Your license for Duende IdentityServer only permits 5 number of clients. You have processed requests for 11 clients and are beyond the threshold of 5 for exceeding permitted clients. In a future version of client limit will be enforced. The clients used were:")); + r.Message.StartsWith("Your IdentityServer license includes 5 clients but you have processed requests for 11 clients")); } [Fact] @@ -213,10 +212,8 @@ public class LicenseUsageTests _licenseUsageTracker.IssuerUsed("issuer2"); var initialLogSnapshot = _logger.Collector.GetSnapshot(); - initialLogSnapshot.ShouldContain(r => - r.Level == LogLevel.Error && - r.Message.StartsWith( - $"You do not have a license, and you have processed requests for 2 issuers. If you are deliberately hosting multiple URLs then this number requires a license per issuer, or the Enterprise Edition tier of license. If not then this might be due to your server being accessed via different URLs or a direct IP and/or you have reverse proxy or a gateway involved, and this suggests a network infrastructure configuration problem. The issuers used were: ")); + initialLogSnapshot.ShouldContain(r => r.Level == LogLevel.Error && r.Message == + "You are using IdentityServer in trial mode and have processed requests for 2 issuers. This indicates that requests for each issuer are being sent to this instance of IdentityServer, which may be due to a network infrastructure configuration issue. If you intend to use multiple issuers, either a license per issuer or an Enterprise Edition license is required. In a future version, this limit will be enforced after a threshold is exceeded. The issuers used were: issuer1, issuer2. See https://duende.link/l/trial for more information."); } [Fact] @@ -228,12 +225,8 @@ public class LicenseUsageTests _licenseUsageTracker.IssuerUsed("issuer2"); var logSnapshot = _logger.Collector.GetSnapshot(); - logSnapshot.ShouldContain(r => - r.Level == LogLevel.Error && - r.Message.StartsWith( - "Your license for Duende IdentityServer only permits 1 number of issuers. You have processed requests for 2 issuers and are still within the threshold of 1. The issuers used were: ") && - r.Message.EndsWith( - "This might be due to your server being accessed via different URLs or a direct IP and/or you have reverse proxy or a gateway involved. This suggests a network infrastructure configuration problem, or you are deliberately hosting multiple URLs and require an upgraded license. In a future version of issuer limit will be enforced.")); + logSnapshot.ShouldContain(r => r.Level == LogLevel.Error && r.Message == + "Your license for IdentityServer includes 1 issuer(s) but you have processed requests for 2 issuers. This indicates that requests for each issuer are being sent to this instance of IdentityServer, which may be due to a network infrastructure configuration issue. If you intend to use multiple issuers, please contact joe@duendesoftware.com from _test or start a conversation with us at https://duende.link/l/contact to upgrade your license as soon as possible. In a future version, this limit will be enforced after a threshold is exceeded. The issuers used were issuer1, issuer2. See https://duende.link/l/threshold for more information."); } [Fact] @@ -258,12 +251,8 @@ public class LicenseUsageTests _licenseUsageTracker.IssuerUsed("issuer3"); var logSnapshot = _logger.Collector.GetSnapshot(); - logSnapshot.ShouldContain(r => - r.Level == LogLevel.Error && - r.Message.StartsWith( - "Your license for Duende IdentityServer only permits 1 number of issuers. You have processed requests for 3 issuers and are over the threshold of 1. The issuers used were: ") && - r.Message.EndsWith( - "This might be due to your server being accessed via different URLs or a direct IP and/or you have reverse proxy or a gateway involved. This suggests a network infrastructure configuration problem, or you are deliberately hosting multiple URLs and require an upgraded license. In a future version of issuer limit will be enforced.")); + logSnapshot.ShouldContain(r => r.Level == LogLevel.Error && r.Message == + "Your license for IdentityServer includes 1 issuer(s) but you have processed requests for 3 issuers. This indicates that requests for each issuer are being sent to this instance of IdentityServer, which may be due to a network infrastructure configuration issue. If you intend to use multiple issuers, please contact joe@duendesoftware.com from _test or start a conversation with us at https://duende.link/l/contact to upgrade your license as soon as possible. In a future version, this limit will be enforced after a threshold is exceeded. The issuers used were issuer3, issuer1, issuer2. See https://duende.link/l/threshold for more information."); } [Fact] diff --git a/identity-server/test/IdentityServer.UnitTests/Licensing/v2/ProtocolRequestCounterTests.cs b/identity-server/test/IdentityServer.UnitTests/Licensing/v2/ProtocolRequestCounterTests.cs index 0aef6c00d..0ea9e3dd4 100644 --- a/identity-server/test/IdentityServer.UnitTests/Licensing/v2/ProtocolRequestCounterTests.cs +++ b/identity-server/test/IdentityServer.UnitTests/Licensing/v2/ProtocolRequestCounterTests.cs @@ -42,7 +42,7 @@ public class ProtocolRequestCounterTests // REMINDER - If this test needs to change because the log message was updated, so should warning_is_not_logged_before_too_many_protocol_requests_are_handled var logRecord = _logger.Collector.GetSnapshot().Single(); logRecord.Message.ShouldBe( - $"You are using IdentityServer in trial mode and have exceeded the trial threshold of {_counter.Threshold} requests handled by IdentityServer. In a future version, you will need to restart the server or configure a license key to continue testing. For more information, please see http://duende.link/trialmode."); + $"You are using IdentityServer in trial mode and have exceeded the trial threshold of {_counter.Threshold} requests handled by IdentityServer. In a future version, you will need to restart the server or configure a license key to continue testing. See https://duende.link/l/trial for more information."); } [Fact] @@ -56,6 +56,6 @@ public class ProtocolRequestCounterTests var logRecords = _logger.Collector.GetSnapshot().Select(l => l.Message); logRecords.ShouldNotContain( - $"You are using IdentityServer in trial mode and have exceeded the trial threshold of {_counter.Threshold} requests handled by IdentityServer. In a future version, you will need to restart the server or configure a license key to continue testing. For more information, please see http://duende.link/trialmode."); + $"You are using IdentityServer in trial mode and have exceeded the trial threshold of {_counter.Threshold} requests handled by IdentityServer. In a future version, you will need to restart the server or configure a license key to continue testing. See https://duende.link/l/trial for more information."); } } diff --git a/identity-server/test/IdentityServer.UnitTests/Services/Default/DefaultSessionCoordinationServiceTests.cs b/identity-server/test/IdentityServer.UnitTests/Services/Default/DefaultSessionCoordinationServiceTests.cs new file mode 100644 index 000000000..59f147431 --- /dev/null +++ b/identity-server/test/IdentityServer.UnitTests/Services/Default/DefaultSessionCoordinationServiceTests.cs @@ -0,0 +1,39 @@ +// Copyright (c) Duende Software. All rights reserved. +// See LICENSE in the project root for license information. + +using Duende.IdentityServer.Configuration; +using Duende.IdentityServer.Models; +using Duende.IdentityServer.Services; +using Duende.IdentityServer.Stores; +using Microsoft.Extensions.Logging.Abstractions; +using UnitTests.Endpoints.EndSession; + +namespace UnitTests.Services.Default; + +public class DefaultSessionCoordinationServiceTests +{ + public DefaultSessionCoordinationService Service; + + [Fact] + public async Task Handles_missing_client_null_reference() + { + var stubBackChannelLogoutClient = new StubBackChannelLogoutClient(); + Service = new DefaultSessionCoordinationService( + new IdentityServerOptions(), + new InMemoryPersistedGrantStore(), + new InMemoryClientStore([]), + stubBackChannelLogoutClient, + new NullLogger()); + + await Service.ProcessExpirationAsync(new UserSession + { + ClientIds = ["not_found"], + SessionId = "1", + SubjectId = "1" + }); + + stubBackChannelLogoutClient + .SendLogoutsWasCalled + .ShouldBeFalse(); + } +}