mirror of
https://github.com/DuendeSoftware/products
synced 2026-05-24 09:28:24 +00:00
Merge pull request #2143 from DuendeSoftware/jmdc/merge-7.3-forward
merge 7.3 forward
This commit is contained in:
commit
753d5cec0f
38 changed files with 608 additions and 217 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -228,4 +228,4 @@ artifacts
|
|||
|
||||
*.Artifacts/
|
||||
|
||||
reports
|
||||
reports
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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()]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ internal class RegisteredImplementationsDiagnosticEntry(ServiceCollectionAccesso
|
|||
}
|
||||
};
|
||||
|
||||
public Task WriteAsync(Utf8JsonWriter writer)
|
||||
public Task WriteAsync(DiagnosticContext context, Utf8JsonWriter writer)
|
||||
{
|
||||
writer.WriteStartObject("RegisteredImplementations");
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -7,5 +7,5 @@ namespace Duende.IdentityServer.Licensing.V2.Diagnostics;
|
|||
|
||||
internal interface IDiagnosticEntry
|
||||
{
|
||||
Task WriteAsync(Utf8JsonWriter writer);
|
||||
public Task WriteAsync(DiagnosticContext context, Utf8JsonWriter writer);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue