Merge pull request #2143 from DuendeSoftware/jmdc/merge-7.3-forward

merge 7.3 forward
This commit is contained in:
Joe DeCock 2025-07-24 21:03:21 -05:00 committed by GitHub
commit 753d5cec0f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 608 additions and 217 deletions

2
.gitignore vendored
View file

@ -228,4 +228,4 @@ artifacts
*.Artifacts/
reports
reports

View file

@ -228,7 +228,11 @@ public static class IdentityServerBuilderExtensionsCore
builder.Services.AddSingleton<IDiagnosticEntry, ClientInfoDiagnosticEntry>();
builder.Services.AddSingleton<ResourceLoadedTracker>();
builder.Services.AddSingleton<IDiagnosticEntry, ResourceInfoDiagnosticEntry>();
builder.Services.AddSingleton<DiagnosticSummary>();
builder.Services.AddSingleton(serviceProvider => new DiagnosticSummary(
DateTime.UtcNow,
serviceProvider.GetServices<IDiagnosticEntry>(),
serviceProvider.GetRequiredService<IdentityServerOptions>(),
serviceProvider.GetRequiredService<ILoggerFactory>()));
builder.Services.AddHostedService<DiagnosticHostedService>();
return builder;

View file

@ -50,6 +50,9 @@ public class LoggingOptions
public ICollection<string> AuthorizeRequestSensitiveValuesFilter { get; set; } =
new HashSet<string>
{
// 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.
/// </summary>
/// <remarks>Please be aware that initializing this property could expose sensitive information in your logs.</remarks>
/// <remarks>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 <see cref="AuthorizeRequestSensitiveValuesFilter"/>
/// </remarks>
public ICollection<string> PushedAuthorizationSensitiveValuesFilter { get; set; } =
new HashSet<string>
{
OidcConstants.TokenRequest.ClientSecret,
OidcConstants.TokenRequest.ClientAssertion,
OidcConstants.AuthorizeRequest.IdTokenHint,
OidcConstants.AuthorizeRequest.Request
};

View file

@ -23,9 +23,18 @@ public class MutualTlsOptions
/// <summary>
/// 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.
/// </summary>
/// <remarks>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.
/// </remarks>
public string? DomainName { get; set; }
/// <summary>

View file

@ -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<ProtectedR
errorDescription = "The access token expired";
}
var errorString = string.Format(CultureInfo.InvariantCulture, $"error=\"{error}\"");
if (errorDescription.IsMissing())
var values = new List<string>
{
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;
}
}

View file

@ -35,64 +35,116 @@ public class MutualTlsEndpointMiddleware
_sanitizedLogger = new SanitizedLogger<MutualTlsEndpointMiddleware>(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;
}
/// <inheritdoc />
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<AuthenticateResult> TriggerCertificateAuthentication(HttpContext context)
{
var x509AuthResult = await context.AuthenticateAsync(_options.MutualTls.ClientCertificateAuthenticationScheme);

View file

@ -105,13 +105,14 @@ internal class IdentityServerLicenseValidator : LicenseValidator<IdentityServerL
EnsureAdded(ref _clientIds, _clientIdLock, clientId);
// Only log for redistribution case because license v2 logs all other cases
if (license != null && license.RedistributionFeature)
{
if (_clientIds.Count > 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<IdentityServerL
EnsureAdded(ref _issuers, _issuerLock, iss);
// Only log for redistribution case because license v2 logs all other cases
if (license != null && license.RedistributionFeature)
{
if (_issuers.Count > 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()]);
}
}

View file

@ -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);

View file

@ -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");

View file

@ -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();

View file

@ -7,11 +7,13 @@ namespace Duende.IdentityServer.Licensing.V2.Diagnostics.DiagnosticEntries;
internal class BasicServerInfoDiagnosticEntry(Func<string> 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();

View file

@ -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");

View file

@ -10,7 +10,7 @@ namespace Duende.IdentityServer.Licensing.V2.Diagnostics.DiagnosticEntries;
internal class DataProtectionDiagnosticEntry(IOptions<DataProtectionOptions> dataProtectionOptions, IOptions<KeyManagementOptions> 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");

View file

@ -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");

View file

@ -23,7 +23,7 @@ internal class IdentityServerOptionsDiagnosticEntry(IOptions<IdentityServerOptio
WriteIndented = false
};
public Task WriteAsync(Utf8JsonWriter writer)
public Task WriteAsync(DiagnosticContext context, Utf8JsonWriter writer)
{
writer.WritePropertyName("IdentityServerOptions");

View file

@ -7,7 +7,7 @@ namespace Duende.IdentityServer.Licensing.V2.Diagnostics.DiagnosticEntries;
internal class LicenseUsageDiagnosticEntry(LicenseUsageTracker licenseUsageTracker) : IDiagnosticEntry
{
public Task WriteAsync(Utf8JsonWriter writer)
public Task WriteAsync(DiagnosticContext context, Utf8JsonWriter writer)
{
writer.WriteStartObject("LicenseUsageSummary");

View file

@ -160,7 +160,7 @@ internal class RegisteredImplementationsDiagnosticEntry(ServiceCollectionAccesso
}
};
public Task WriteAsync(Utf8JsonWriter writer)
public Task WriteAsync(DiagnosticContext context, Utf8JsonWriter writer)
{
writer.WriteStartObject("RegisteredImplementations");

View file

@ -7,7 +7,7 @@ namespace Duende.IdentityServer.Licensing.V2.Diagnostics.DiagnosticEntries;
internal class ResourceInfoDiagnosticEntry(ResourceLoadedTracker resourceLoadedTracker) : IDiagnosticEntry
{
public Task WriteAsync(Utf8JsonWriter writer)
public Task WriteAsync(DiagnosticContext context, Utf8JsonWriter writer)
{
writer.WriteStartObject("Resources");

View file

@ -13,18 +13,19 @@ internal class TokenIssueCountDiagnosticEntry : IDiagnosticEntry, IDisposable
private long _jwtTokenIssued;
private long _referenceTokenIssued;
private long _refreshTokenIssued;
private long _jwtDPoPTokenIssued;
private long _referenceDPoPTokenIssued;
private long _jwtMTLSTokenIssued;
private long _referenceMTLSTokenIssued;
private long _idTokenIssued;
private long _tokensWithNoConstraint;
private long _tokensWithDPoPConstraint;
private long _tokensWithMtlsConstraint;
private long _implicitGrantTypeFlows;
private long _hybridGrantTypeFlows;
private long _authorizationCodeGrantTypeFlows;
private long _clientCredentialsGrantTypeFlows;
private long _resourceOwnerPasswordGrantTypeFlows;
private long _deviceFlowGrantTypeFlows;
private long _refreshTokenGrantTypeFlows;
private long _otherGrantTypeFlows;
private readonly MeterListener _meterListener;
@ -46,26 +47,38 @@ internal class TokenIssueCountDiagnosticEntry : IDiagnosticEntry, IDisposable
_meterListener.Start();
}
public Task WriteAsync(Utf8JsonWriter writer)
public Task WriteAsync(DiagnosticContext context, Utf8JsonWriter writer)
{
writer.WritePropertyName("TokenIssueCounts");
writer.WriteStartObject();
writer.WriteNumber("Jwt", _jwtTokenIssued);
writer.WriteNumber("Reference", _referenceTokenIssued);
writer.WriteNumber("JwtDPoP", _jwtDPoPTokenIssued);
writer.WriteNumber("ReferenceDPoP", _referenceDPoPTokenIssued);
writer.WriteNumber("JwtMTLS", _jwtMTLSTokenIssued);
writer.WriteNumber("ReferenceMTLS", _referenceMTLSTokenIssued);
writer.WriteNumber("Refresh", _refreshTokenIssued);
writer.WriteNumber("Id", _idTokenIssued);
writer.WriteStartObject("RequestsByGrantType");
writer.WriteNumber(GrantType.Implicit, _implicitGrantTypeFlows);
writer.WriteNumber(GrantType.Hybrid, _hybridGrantTypeFlows);
writer.WriteNumber(GrantType.AuthorizationCode, _authorizationCodeGrantTypeFlows);
writer.WriteNumber(GrantType.ClientCredentials, _clientCredentialsGrantTypeFlows);
writer.WriteNumber(GrantType.ResourceOwnerPassword, _resourceOwnerPasswordGrantTypeFlows);
writer.WriteNumber(GrantType.DeviceFlow, _deviceFlowGrantTypeFlows);
writer.WriteNumber(GrantType.RefreshToken, _refreshTokenGrantTypeFlows);
writer.WriteNumber("Other", _otherGrantTypeFlows);
writer.WriteEndObject();
writer.WriteStartObject("AccessTokensByType");
writer.WriteNumber("Jwt", _jwtTokenIssued);
writer.WriteNumber("Reference", _referenceTokenIssued);
writer.WriteEndObject();
writer.WriteStartObject("AccessTokensBySenderConstraint");
writer.WriteNumber("None", _tokensWithNoConstraint);
writer.WriteNumber("DPoP", _tokensWithDPoPConstraint);
writer.WriteNumber("mTLS", _tokensWithMtlsConstraint);
writer.WriteEndObject();
writer.WriteStartObject("TokensByType");
writer.WriteNumber("Access", _jwtTokenIssued + _referenceTokenIssued);
writer.WriteNumber("Refresh", _refreshTokenIssued);
writer.WriteNumber("Id", _idTokenIssued);
writer.WriteEndObject();
writer.WriteEndObject();
@ -119,25 +132,26 @@ internal class TokenIssueCountDiagnosticEntry : IDiagnosticEntry, IDisposable
if (accessTokenIssued)
{
switch (proofType)
switch (accessTokenType)
{
case ProofType.None when accessTokenType == AccessTokenType.Jwt:
case AccessTokenType.Jwt:
Interlocked.Increment(ref _jwtTokenIssued);
break;
case ProofType.None when accessTokenType == AccessTokenType.Reference:
case AccessTokenType.Reference:
Interlocked.Increment(ref _referenceTokenIssued);
break;
case ProofType.DPoP when accessTokenType == AccessTokenType.Jwt:
Interlocked.Increment(ref _jwtDPoPTokenIssued);
}
switch (proofType)
{
case ProofType.None:
Interlocked.Increment(ref _tokensWithNoConstraint);
break;
case ProofType.DPoP when accessTokenType == AccessTokenType.Reference:
Interlocked.Increment(ref _referenceDPoPTokenIssued);
case ProofType.ClientCertificate:
Interlocked.Increment(ref _tokensWithMtlsConstraint);
break;
case ProofType.ClientCertificate when accessTokenType == AccessTokenType.Jwt:
Interlocked.Increment(ref _jwtMTLSTokenIssued);
break;
case ProofType.ClientCertificate when accessTokenType == AccessTokenType.Reference:
Interlocked.Increment(ref _referenceMTLSTokenIssued);
case ProofType.DPoP:
Interlocked.Increment(ref _tokensWithDPoPConstraint);
break;
}
}
@ -178,6 +192,9 @@ internal class TokenIssueCountDiagnosticEntry : IDiagnosticEntry, IDisposable
case GrantType.DeviceFlow:
Interlocked.Increment(ref _deviceFlowGrantTypeFlows);
break;
case GrantType.RefreshToken:
Interlocked.Increment(ref _refreshTokenGrantTypeFlows);
break;
default:
Interlocked.Increment(ref _otherGrantTypeFlows);
break;

View file

@ -9,9 +9,10 @@ using Microsoft.Extensions.Logging;
namespace Duende.IdentityServer.Licensing.V2.Diagnostics;
internal class DiagnosticSummary(IEnumerable<IDiagnosticEntry> entries, IdentityServerOptions options, ILoggerFactory loggerFactory)
internal class DiagnosticSummary(DateTime serverStartTime, IEnumerable<IDiagnosticEntry> entries, IdentityServerOptions options, ILoggerFactory loggerFactory)
{
private readonly ILogger _logger = loggerFactory.CreateLogger("Duende.IdentityServer.Diagnostics.Summary");
public async Task PrintSummary()
{
var bufferWriter = new ArrayBufferWriter<byte>();
@ -19,9 +20,10 @@ internal class DiagnosticSummary(IEnumerable<IDiagnosticEntry> 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();

View file

@ -7,5 +7,5 @@ namespace Duende.IdentityServer.Licensing.V2.Diagnostics;
internal interface IDiagnosticEntry
{
Task WriteAsync(Utf8JsonWriter writer);
public Task WriteAsync(DiagnosticContext context, Utf8JsonWriter writer);
}

View file

@ -21,7 +21,7 @@ internal class LicenseExpirationChecker(
if (!_expiredLicenseWarned && !license.Current.Redistribution && IsExpired)
{
_expiredLicenseWarned = true;
_logger.LicenseHasExpired();
_logger.LicenseHasExpired(license.Current.ContactInfo ?? "<contact info missing>", license.Current.CompanyName ?? "<company name missing>");
}
}

View file

@ -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;
}
}

View file

@ -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<string> 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<string> 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<string> 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<string> 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<string> 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<string> 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<string> 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<string> 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<string> 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<string> issuersUsed);
}

View file

@ -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);
}
}
}

View file

@ -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";
}

View file

@ -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,

View file

@ -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.");
}
}

View file

@ -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);
}
}

View file

@ -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<string> GetResponseBodyAsString(HttpContext context)
{
context.Response.Body.Seek(0, SeekOrigin.Begin);

View file

@ -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"));
}
}

View file

@ -10,14 +10,14 @@ namespace IdentityServer.UnitTests.Licensing.V2.DiagnosticEntries;
internal static class DiagnosticEntryTestHelper
{
public static async Task<JsonDocument> WriteEntryToJson(IDiagnosticEntry subject)
public static async Task<JsonDocument> WriteEntryToJson(IDiagnosticEntry subject, DateTime? serverStartTime = null, DateTime? currentServerTime = null)
{
var bufferWriter = new ArrayBufferWriter<byte>();
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();

View file

@ -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,

View file

@ -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<DiagnosticSummary>();
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<DiagnosticSummary>();
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<DiagnosticSummary>();
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<DiagnosticSummary>();
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;

View file

@ -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]

View file

@ -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]

View file

@ -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.");
}
}

View file

@ -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<DefaultSessionCoordinationService>());
await Service.ProcessExpirationAsync(new UserSession
{
ClientIds = ["not_found"],
SessionId = "1",
SubjectId = "1"
});
stubBackChannelLogoutClient
.SendLogoutsWasCalled
.ShouldBeFalse();
}
}