Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,8 @@ public static IIdentityServerBuilder AddCoreServices(this IIdentityServerBuilder
builder.Services.AddSingleton<IDiagnosticEntry, LicenseUsageDiagnosticEntry>();
builder.Services.AddSingleton<IDiagnosticEntry>(new BasicServerInfoDiagnosticEntry(Dns.GetHostName));
builder.Services.AddSingleton<IDiagnosticEntry, EndpointUsageDiagnosticEntry>();
builder.Services.AddSingleton<ClientLoadedTracker>();
builder.Services.AddSingleton<IDiagnosticEntry, ClientInfoDiagnosticEntry>();
builder.Services.AddSingleton<DiagnosticSummary>();
builder.Services.AddHostedService<DiagnosticHostedService>();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.

using System.Text.Json.Serialization.Metadata;

namespace Duende.IdentityServer.Infrastructure;

public class RemovePropertyModifier<T>(List<string> propertiesToRemove)
{
public void ModifyTypeInfo(JsonTypeInfo typeInfo)
{
if (typeInfo.Type != typeof(T))
{
return;
}

var propsToKeep = typeInfo.Properties.Where(propertyInfo => !propertiesToRemove.Contains(propertyInfo.Name)).ToArray();

typeInfo.Properties.Clear();
foreach (var prop in propsToKeep)
{
typeInfo.Properties.Add(prop);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.

#nullable enable
using System.Collections.Concurrent;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using Duende.IdentityServer.Models;

namespace Duende.IdentityServer.Licensing.V2.Diagnostics;

internal class ClientLoadedTracker : IDisposable
{
private const int MaxClientsTrackedCount = 100;
private const int ArrayMaxSize = 10;

private int _clientCount;

private readonly ConcurrentDictionary<string, JsonObject> _clients = new();
private readonly List<string> _propertiesToExclude = [nameof(Client.Properties), nameof(Client.LogoUri), nameof(Client.Claims)];
private readonly JsonDocument _defaultClient;
private readonly JsonSerializerOptions _serializerOptions = new()
{
Converters = { new JsonStringEnumConverter() },
WriteIndented = false
};

public ClientLoadedTracker() => _defaultClient = JsonSerializer.SerializeToDocument(new Client(), _serializerOptions);

public void TrackClientLoaded(Client client)
{
if (_clientCount >= MaxClientsTrackedCount)
{
return;
}

using var clientJson = JsonSerializer.SerializeToDocument(client, _serializerOptions);
var clientDiagnosticData = new JsonObject();
foreach (var property in _defaultClient.RootElement.EnumerateObject())
{
if (_propertiesToExclude.Contains(property.Name))
{
continue;
}

if (!_defaultClient.RootElement.TryGetProperty(property.Name, out var defaultValue) ||
!clientJson.RootElement.TryGetProperty(property.Name, out var clientValue))
{
continue;
}

if (property.NameEquals(nameof(Client.ClientSecrets)))
{
var secrets = clientValue.EnumerateArray()
.Select(secret => secret.GetProperty(nameof(Secret.Type)).GetString())
.Distinct()
.Select(secret => JsonValue.Create(secret))
.Cast<JsonNode>();
clientDiagnosticData["SecretTypes"] = new JsonArray(secrets.ToArray());
}
else if (defaultValue.ValueKind == JsonValueKind.Array && clientValue.ValueKind == JsonValueKind.Array && clientValue.GetArrayLength() > 0)
{
var arrayEntries = clientValue.EnumerateArray().Take(ArrayMaxSize).Select(CreateJsonValue).Cast<JsonNode>();
clientDiagnosticData[property.Name] = new JsonArray(arrayEntries.ToArray());
}
else
{
if (!JsonElementEquals(defaultValue, clientValue))
{
clientDiagnosticData[property.Name] = CreateJsonValue(clientValue);
}
}
}

if (_clients.ContainsKey(client.ClientId))
{
return;
}

if (_clients.TryAdd(client.ClientId, clientDiagnosticData))
{
Interlocked.Increment(ref _clientCount);
}
}

private bool JsonElementEquals(JsonElement a, JsonElement b)
{
if (a.ValueKind != b.ValueKind)
{
return false;
}

return a.ValueKind switch
{
JsonValueKind.String => a.GetString() == b.GetString(),
JsonValueKind.Number => a.GetDouble().CompareTo(b.GetDouble()) == 0,
JsonValueKind.True or JsonValueKind.False => a.GetBoolean() == b.GetBoolean(),
_ => a.ToString() == b.ToString()
};
}

private JsonValue? CreateJsonValue(JsonElement element) => element.ValueKind switch
{
JsonValueKind.String => JsonValue.Create(element.GetString()),
JsonValueKind.Number => JsonValue.Create(element.GetDouble()),
JsonValueKind.True or JsonValueKind.False => JsonValue.Create(element.GetBoolean()),
_ => JsonValue.Create(element.ToString())
};

public IReadOnlyDictionary<string, JsonObject> Clients => _clients;

public void Dispose() => _defaultClient.Dispose();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.

#nullable enable
using System.Text.Json;

namespace Duende.IdentityServer.Licensing.V2.Diagnostics.DiagnosticEntries;

internal class ClientInfoDiagnosticEntry(ClientLoadedTracker clientLoadedTracker) : IDiagnosticEntry
{
private readonly JsonSerializerOptions _serializerOptions = new()
{
WriteIndented = false
};

public Task WriteAsync(Utf8JsonWriter writer)
{
writer.WriteStartArray("Clients");

foreach (var (clientId, client) in clientLoadedTracker.Clients)
{
JsonSerializer.Serialize(writer, client, _serializerOptions);
}

writer.WriteEndArray();

return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,21 @@
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using Duende.IdentityServer.Configuration;
using Duende.IdentityServer.Infrastructure;
using Microsoft.Extensions.Options;

namespace Duende.IdentityServer.Licensing.V2.Diagnostics.DiagnosticEntries;

internal class IdentityServerOptionsDiagnosticEntry(IOptions<IdentityServerOptions> options) : IDiagnosticEntry
{
private static readonly RemovePropertyModifier<IdentityServerOptions> RemoveLicenseKeyModifier = new([
nameof(IdentityServerOptions.LicenseKey)
]);
private readonly JsonSerializerOptions _serializerOptions = new()
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver
{
Modifiers = { RemoveLicenseKeyModifier }
Modifiers = { RemoveLicenseKeyModifier.ModifyTypeInfo }
},
WriteIndented = false
};
Expand All @@ -27,22 +31,4 @@ public Task WriteAsync(Utf8JsonWriter writer)

return Task.CompletedTask;
}

private static void RemoveLicenseKeyModifier(JsonTypeInfo typeInfo)
{
if (typeInfo.Type != typeof(IdentityServerOptions))
{
return;
}

var propsToKeep = typeInfo.Properties
.Where(prop => prop.Name != nameof(IdentityServerOptions.LicenseKey))
.ToArray();

typeInfo.Properties.Clear();
foreach (var prop in propsToKeep)
{
typeInfo.Properties.Add(prop);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Duende.IdentityServer.Configuration;
using Duende.IdentityServer.Extensions;
using Duende.IdentityServer.Licensing.V2;
using Duende.IdentityServer.Licensing.V2.Diagnostics;
using Duende.IdentityServer.Logging;
using Duende.IdentityServer.Logging.Models;
using Duende.IdentityServer.Models;
Expand All @@ -28,6 +29,7 @@ internal class AuthorizeRequestValidator : IAuthorizeRequestValidator
private readonly IUserSession _userSession;
private readonly IRequestObjectValidator _requestObjectValidator;
private readonly LicenseUsageTracker _licenseUsage;
private readonly ClientLoadedTracker _clientLoadedTracker;
private readonly SanitizedLogger<AuthorizeRequestValidator> _sanitizedLogger;

private readonly ResponseTypeEqualityComparer
Expand All @@ -44,6 +46,7 @@ public AuthorizeRequestValidator(
IUserSession userSession,
IRequestObjectValidator requestObjectValidator,
LicenseUsageTracker licenseUsage,
ClientLoadedTracker clientLoadedTracker,
SanitizedLogger<AuthorizeRequestValidator> sanitizedLogger)
{
_options = options;
Expand All @@ -55,6 +58,7 @@ public AuthorizeRequestValidator(
_requestObjectValidator = requestObjectValidator;
_userSession = userSession;
_licenseUsage = licenseUsage;
_clientLoadedTracker = clientLoadedTracker;
_sanitizedLogger = sanitizedLogger;
}

Expand Down Expand Up @@ -144,6 +148,7 @@ public async Task<AuthorizeRequestValidationResult> ValidateAsync(
_sanitizedLogger.LogTrace("Authorize request protocol validation successful");

_licenseUsage.ClientUsed(request.ClientId);
_clientLoadedTracker.TrackClientLoaded(request.Client);
IdentityServerLicenseValidator.Instance.ValidateClient(request.ClientId);

return Valid(request);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Duende.IdentityServer.Events;
using Duende.IdentityServer.Extensions;
using Duende.IdentityServer.Licensing.V2;
using Duende.IdentityServer.Licensing.V2.Diagnostics;
using Duende.IdentityServer.Logging.Models;
using Duende.IdentityServer.Models;
using Duende.IdentityServer.Services;
Expand Down Expand Up @@ -37,6 +38,7 @@ internal class TokenRequestValidator : ITokenRequestValidator
private readonly IBackchannelAuthenticationRequestIdValidator _backchannelAuthenticationRequestIdValidator;
private readonly IClock _clock;
private readonly LicenseUsageTracker _licenseUsage;
private readonly ClientLoadedTracker _clientLoadedTracker;
private readonly ILogger _logger;

private ValidatedTokenRequest _validatedRequest;
Expand All @@ -59,6 +61,7 @@ public TokenRequestValidator(
IEventService events,
IClock clock,
LicenseUsageTracker licenseUsage,
ClientLoadedTracker clientLoadedTracker,
ILogger<TokenRequestValidator> logger)
{
_logger = logger;
Expand All @@ -79,6 +82,7 @@ public TokenRequestValidator(
_refreshTokenService = refreshTokenService;
_dPoPProofValidator = dPoPProofValidator;
_events = events;
_clientLoadedTracker = clientLoadedTracker;
}

// only here for legacy unit tests
Expand Down Expand Up @@ -305,6 +309,7 @@ private async Task<TokenRequestValidationResult> RunValidationAsync(Func<NameVal

var clientId = customValidationContext.Result.ValidatedRequest.ClientId;
_licenseUsage.ClientUsed(clientId);
_clientLoadedTracker.TrackClientLoaded(customValidationContext.Result.ValidatedRequest.Client);
IdentityServerLicenseValidator.Instance.ValidateClient(clientId);

return customValidationContext.Result;
Expand Down Expand Up @@ -443,7 +448,7 @@ private async Task<TokenRequestValidationResult> ValidateAuthorizationCodeReques
}

//////////////////////////////////////////////////////////
// resource and scope validation
// resource and scope validation
//////////////////////////////////////////////////////////
var validatedResources = await _resourceValidator.ValidateRequestedResourcesAsync(new ResourceValidationRequest
{
Expand Down Expand Up @@ -791,7 +796,7 @@ private async Task<TokenRequestValidationResult> ValidateRefreshTokenRequestAsyn
}

//////////////////////////////////////////////////////////
// resource and scope validation
// resource and scope validation
//////////////////////////////////////////////////////////
var validatedResources = await _resourceValidator.ValidateRequestedResourcesAsync(new ResourceValidationRequest
{
Expand Down Expand Up @@ -873,7 +878,7 @@ private async Task<TokenRequestValidationResult> ValidateDeviceCodeRequestAsync(
}

//////////////////////////////////////////////////////////
// scope validation
// scope validation
//////////////////////////////////////////////////////////
var validatedResources = await _resourceValidator.ValidateRequestedResourcesAsync(new ResourceValidationRequest
{
Expand Down Expand Up @@ -962,7 +967,7 @@ private async Task<TokenRequestValidationResult> ValidateCibaRequestRequestAsync
}

//////////////////////////////////////////////////////////
// resource and scope validation
// resource and scope validation
//////////////////////////////////////////////////////////
var validatedResources = await _resourceValidator.ValidateRequestedResourcesAsync(new ResourceValidationRequest
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.

using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using Duende.IdentityServer.Infrastructure;

namespace IdentityServer.UnitTests.Infrastructure;

public class RemovePropertyModifierTests
{
[Fact]
public void ShouldRemoveSpecifiedProperties()
{
var testObject = new TestClass { Property1 = "Value1", Property2 = "Value2", Property3 = "Value3" };
var modifier = new RemovePropertyModifier<TestClass>([nameof(TestClass.Property1)]);
var serializerOptions = new JsonSerializerOptions
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver
{
Modifiers = { modifier.ModifyTypeInfo }
}
};

var result = JsonSerializer.Serialize(testObject, serializerOptions);

var json = JsonDocument.Parse(result);
json.RootElement.TryGetProperty("Property1", out _).ShouldBeFalse();
json.RootElement.TryGetProperty("Property2", out var property2).ShouldBeTrue();
property2.GetString().ShouldBe(testObject.Property2);
json.RootElement.TryGetProperty("Property3", out var property3).ShouldBeTrue();
property3.GetString().ShouldBe(testObject.Property3);
}

private class TestClass
{
public string Property1 { get; init; }
public string Property2 { get; init; }
public string Property3 { get; init; }
}
}
Loading
Loading