Skip to content
Closed
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 @@ -213,6 +213,12 @@ public static IIdentityServerBuilder AddCoreServices(this IIdentityServerBuilder
builder.Services.AddSingleton<LicenseExpirationChecker>();

builder.Services.AddSingleton<IDiagnosticEntry, AssemblyInfoDiagnosticEntry>();
builder.Services.AddSingleton<IDiagnosticEntry, AuthSchemeInfoDiagnosticEntry>();
builder.Services.AddSingleton<IDiagnosticEntry, RegisteredImplementationsDiagnosticEntry>();
builder.Services.AddSingleton<IDiagnosticEntry, IdentityServerOptionsDiagnosticEntry>();
builder.Services.AddSingleton<IDiagnosticEntry, DataProtectionDiagnosticEntry>();
builder.Services.AddSingleton<IDiagnosticEntry, TokenIssueCountDiagnosticEntry>();
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
Expand Up @@ -239,7 +239,12 @@ private Task RaiseResponseEventAsync(AuthorizeResponse response)
Telemetry.Metrics.TokenIssued(
response.Request.ClientId,
response.Request.GrantType,
response.Request.AuthorizeRequestType);
response.Request.AuthorizeRequestType,
response.AccessToken.IsPresent(),
response.AccessToken.IsPresent() ? response.Request.AccessTokenType : null,
false,
ProofType.None,
response.IdentityToken.IsPresent());
return _events.RaiseAsync(new TokenIssuedSuccessEvent(response));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,10 @@ private async Task<IEndpointResult> ProcessTokenRequestAsync(HttpContext context
var response = await _responseGenerator.ProcessAsync(requestResult);

await _events.RaiseAsync(new TokenIssuedSuccessEvent(response, requestResult));
Telemetry.Metrics.TokenIssued(clientResult.Client.ClientId, requestResult.ValidatedRequest.GrantType, null);

Telemetry.Metrics.TokenIssued(clientResult.Client.ClientId, requestResult.ValidatedRequest.GrantType, null,
response.AccessToken.IsPresent(), response.AccessTokenType.IsPresent() ? requestResult.ValidatedRequest.AccessTokenType : null, response.RefreshToken.IsPresent(),
requestResult.ValidatedRequest.ProofType, response.IdentityToken.IsPresent());
LogTokens(response, requestResult);

// return result
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public static async Task<Client> FindEnabledClientByIdAsync(this IClientStore st
var client = await store.FindClientByIdAsync(clientId);
if (client != null && client.Enabled)
{
Telemetry.Metrics.ClientLoaded(clientId);
return client;
}

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);
}
}
}
13 changes: 13 additions & 0 deletions identity-server/src/IdentityServer/Licensing/V2/AtomicCounter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.

namespace Duende.IdentityServer.Licensing.V2;

internal class AtomicCounter(int initialCount = 0)
{
private long _count = initialCount;

public void Increment() => Interlocked.Increment(ref _count);

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

using System.Text.Json;
using Microsoft.AspNetCore.Authentication;

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

internal class AuthSchemeInfoDiagnosticEntry(IAuthenticationSchemeProvider authenticationSchemeProvider) : IDiagnosticEntry
{
public async Task WriteAsync(Utf8JsonWriter writer)
{
var schemes = await authenticationSchemeProvider.GetAllSchemesAsync();

writer.WriteStartObject("AuthSchemeInfo");
writer.WriteStartArray("Schemes");
foreach (var scheme in schemes)
{
writer.WriteStartObject();
writer.WriteString(scheme.Name, scheme.HandlerType.FullName ?? "Unknown");
writer.WriteEndObject();
}
writer.WriteEndArray();
writer.WriteEndObject();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.

#nullable enable
using System.Collections.Concurrent;
using System.Diagnostics.Metrics;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using Duende.IdentityServer.Infrastructure;
using Duende.IdentityServer.Models;
using Duende.IdentityServer.Stores;
using Microsoft.Extensions.Logging;

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

internal class ClientInfoDiagnosticEntry : IDiagnosticEntry
{
private static readonly RemovePropertyModifier<Client> RemoveSecretsModifier = new(
[
nameof(Client.ClientSecrets)
]);
private readonly JsonSerializerOptions _serializerOptions = new()
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver
{
Modifiers = { RemoveSecretsModifier.ModifyTypeInfo }
},
WriteIndented = false
};

private readonly IClientStore _clientStore;
private readonly ILogger<ClientInfoDiagnosticEntry> _logger;
private readonly ConcurrentDictionary<string, Client> _clients = new();
private readonly MeterListener _meterListener;

public ClientInfoDiagnosticEntry(IClientStore clientStore, ILogger<ClientInfoDiagnosticEntry> logger)
{
_clientStore = clientStore;
_logger = logger;
_meterListener = new MeterListener();

_meterListener.InstrumentPublished += (instrument, listener) =>
{
if (instrument.Name == Telemetry.Metrics.Counters.ClientLoaded)
{
listener.EnableMeasurementEvents(instrument);
}
};

_meterListener.SetMeasurementEventCallback<long>(HandleClientMeasurementRecorded);

_meterListener.Start();
}

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

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

writer.WriteEndObject();

return Task.CompletedTask;
}

private void HandleClientMeasurementRecorded(Instrument instrument, long measurement, ReadOnlySpan<KeyValuePair<string, object?>> tags, object? state)
{
if (instrument.Name != Telemetry.Metrics.Counters.ClientLoaded)
{
return;
}

string? clientId = null;
foreach (var tag in tags)
{
if (tag is { Key: Telemetry.Metrics.Tags.Client, Value: string id })
{
clientId = id;
}
}

if (clientId != null && !_clients.ContainsKey(clientId))
{
_ = LoadClientAsync(clientId);
}
}

private async Task LoadClientAsync(string clientId)
{
try
{
if (_clients.ContainsKey(clientId))
{
return;
}

//It's important to use FindClientByIdAsync here since we are responding to a measurement event
//which is triggered by the use of FindEnabledClientByIdAsync. We also do not care if the client
//is enabled or not at this point. If it was loaded, we want to see it in the diagnostics.
var client = await _clientStore.FindClientByIdAsync(clientId);
if (client != null)
{
_clients.TryAdd(clientId, client);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error adding client {ClientId} to diagnostics", clientId);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.

using System.Text.Json;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.Extensions.Options;

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

internal class DataProtectionDiagnosticEntry(IOptions<DataProtectionOptions> dataProtectionOptions, IOptions<KeyManagementOptions> keyManagementOptions) : IDiagnosticEntry
{
public Task WriteAsync(Utf8JsonWriter writer)
{
writer.WriteStartObject("DataProtectionConfiguration");
writer.WriteString("ApplicationDiscriminator", dataProtectionOptions?.Value?.ApplicationDiscriminator ?? "Not Configured");
writer.WriteString("XmlEncryptor", keyManagementOptions?.Value.XmlEncryptor?.GetType().FullName ?? "Not Configured");
writer.WriteString("XmlRepository", keyManagementOptions?.Value.XmlRepository?.GetType().FullName ?? "Not Configured");
writer.WriteEndObject();

return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// 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.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.ModifyTypeInfo }
},
WriteIndented = false
};

public Task WriteAsync(Utf8JsonWriter writer)
{
writer.WritePropertyName("IdentityServerOptions");

JsonSerializer.Serialize(writer, options.Value, _serializerOptions);

return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.

using System.Text.Json;
using Duende.IdentityServer.Services;
using Duende.IdentityServer.Stores;
using Microsoft.Extensions.DependencyInjection;

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

internal class RegisteredImplementationsDiagnosticEntry(IServiceProvider serviceProvider) : IDiagnosticEntry
{
public Task WriteAsync(Utf8JsonWriter writer)
{
using var scope = serviceProvider.CreateScope();

writer.WriteStartObject("RegisteredServices");

writer.WriteStartArray("Services");

//services
InspectService<IBackchannelAuthenticationInteractionService>(nameof(IBackchannelAuthenticationInteractionService), writer, scope);
InspectService<IBackchannelAuthenticationThrottlingService>(nameof(IBackchannelAuthenticationThrottlingService), writer, scope);
InspectService<IBackchannelAuthenticationUserNotificationService>(nameof(IBackchannelAuthenticationUserNotificationService), writer, scope);
InspectService<IBackChannelLogoutService>(nameof(IBackChannelLogoutService), writer, scope);
InspectService(typeof(ICache<>), "ICache", writer, scope); //TODO: replace with nameof operator when C# 14 is available with support for nameof operator on open generic types
InspectService<IClaimsService>(nameof(IClaimsService), writer, scope);
InspectService<IConsentService>(nameof(IConsentService), writer, scope);
InspectService<IDeviceFlowCodeService>(nameof(IDeviceFlowCodeService), writer, scope);
InspectService<IDeviceFlowInteractionService>(nameof(IDeviceFlowCodeService), writer, scope);
InspectService<IDeviceFlowThrottlingService>(nameof(IDeviceFlowThrottlingService), writer, scope);
InspectService<IEventService>(nameof(IEventService), writer, scope);
InspectService<IEventSink>(nameof(IEventSink), writer, scope);
InspectService<IHandleGenerationService>(nameof(IHandleGenerationService), writer, scope);
InspectService<IIdentityServerInteractionService>(nameof(IIdentityServerInteractionService), writer, scope);
InspectService<IIssuerNameService>(nameof(IIssuerNameService), writer, scope);
InspectService<IKeyMaterialService>(nameof(IKeyMaterialService), writer, scope);
InspectService<ILogoutNotificationService>(nameof(ILogoutNotificationService), writer, scope);
InspectService<IPersistedGrantService>(nameof(IPersistedGrantService), writer, scope);
InspectService<IProfileService>(nameof(IProfileService), writer, scope);
InspectService<IPushedAuthorizationSerializer>(nameof(IPushedAuthorizationSerializer), writer, scope);
InspectService<IPushedAuthorizationService>(nameof(IPushedAuthorizationService), writer, scope);
InspectService<IRefreshTokenService>(nameof(IRefreshTokenService), writer, scope);
InspectService<IReplayCache>(nameof(IReplayCache), writer, scope);
InspectService<IReturnUrlParser>(nameof(IReturnUrlParser), writer, scope);
InspectService<IServerUrls>(nameof(IServerUrls), writer, scope);
InspectService<ISessionCoordinationService>(nameof(ISessionCoordinationService), writer, scope);
InspectService<ISessionManagementService>(nameof(ISessionManagementService), writer, scope);
InspectService<ITokenCreationService>(nameof(ITokenCreationService), writer, scope);
InspectService<ITokenService>(nameof(ITokenService), writer, scope);
InspectService<IUserCodeGenerator>(nameof(IUserCodeGenerator), writer, scope);
InspectService<IUserCodeService>(nameof(IUserCodeService), writer, scope);
InspectService<IUserSession>(nameof(IUserSession), writer, scope);

//stores
InspectService<IAuthorizationCodeStore>(nameof(IAuthorizationCodeStore), writer, scope);
InspectService<IAuthorizationParametersMessageStore>(nameof(IAuthorizationParametersMessageStore), writer, scope);
InspectService<IClientStore>(nameof(IClientStore), writer, scope);
InspectService<IConsentMessageStore>(nameof(IConsentMessageStore), writer, scope);
InspectService<IDeviceFlowStore>(nameof(IDeviceFlowStore), writer, scope);
InspectService<IIdentityProviderStore>(nameof(IIdentityProviderStore), writer, scope);
InspectService(typeof(IMessageStore<>), "IMessageStore", writer, scope); //TODO: replace with nameof operator when C# 14 is available with support for nameof operator on open generic types
InspectService<IPersistedGrantStore>(nameof(IPersistedGrantStore), writer, scope);
InspectService<IPushedAuthorizationRequestStore>(nameof(IPushedAuthorizationRequestStore), writer, scope);
InspectService<IReferenceTokenStore>(nameof(IReferenceTokenStore), writer, scope);
InspectService<IResourceStore>(nameof(IResourceStore), writer, scope);
InspectService<IServerSideSessionsMarker>(nameof(IServerSideSessionsMarker), writer, scope);
InspectService<IServerSideSessionStore>(nameof(IServerSideSessionStore), writer, scope);
InspectService<IServerSideTicketStore>(nameof(IServerSideTicketStore), writer, scope);
InspectService<ISigningCredentialStore>(nameof(ISigningCredentialStore), writer, scope);
InspectService<ISigningKeyStore>(nameof(ISigningKeyStore), writer, scope);
InspectService<IUserConsentStore>(nameof(IUserConsentStore), writer, scope);
InspectService<IValidationKeysStore>(nameof(IValidationKeysStore), writer, scope);

writer.WriteEndArray();

writer.WriteEndObject();

return Task.CompletedTask;
}

private void InspectService<T>(string serviceName, Utf8JsonWriter writer, IServiceScope scope) where T : class => InspectService(typeof(T), serviceName, writer, scope);

private void InspectService(Type targetType, string serviceName, Utf8JsonWriter writer, IServiceScope scope)
{
writer.WriteStartObject();

writer.WriteStartObject(serviceName);
var service = scope.ServiceProvider.GetService(targetType);
if (service != null)
{
var type = service.GetType();
writer.WriteString("TypeName", type.FullName);
writer.WriteString("Assembly", type.Assembly.GetName().Name);
writer.WriteString("AssemblyVersion", type.Assembly.GetName().Version?.ToString());
}
else
{
writer.WriteString("TypeName", "Not Registered");
writer.WriteString("Assembly", "Not Registered");
writer.WriteString("AssemblyVersion", "Not Registered");
}

writer.WriteEndObject();
writer.WriteEndObject();
}
}
Loading
Loading