diff --git a/Directory.Build.props b/Directory.Build.props index 28b09c336..3715c72ee 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 1.0.1 + 1.0.2 1.0 RockSolidKnowledge CS1591;CS1570;CS1571;CS1572;CS1573;CS1574;CS1580;CS1581;CS1584;CS1587;CS1591;CS1658;CS1712;CS1734 diff --git a/NuGet.config b/NuGet.config index 105c92dac..b7b37a484 100644 --- a/NuGet.config +++ b/NuGet.config @@ -5,4 +5,12 @@ + \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index 1608afae5..273c959de 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -92,6 +92,7 @@ Open.IdentityServer enables the following features in your applications: topics/deployment topics/logging topics/events + topics/open_telemetry topics/crypto topics/grant_types topics/client_authentication diff --git a/docs/topics/open_telemetry.rst b/docs/topics/open_telemetry.rst new file mode 100644 index 000000000..6c4947b95 --- /dev/null +++ b/docs/topics/open_telemetry.rst @@ -0,0 +1,304 @@ +.. _refOpenTelemetry: + +OpenTelemetry +============= + +`OpenTelemetry `_ is an open-source, vendor-neutral observability framework for generating, collecting, and exporting telemetry data (metrics, traces, and logs). It provides a standardised way to instrument applications, making it possible to monitor and troubleshoot distributed systems without being locked in to a particular observability vendor. + +Open.IdentityServer provides built-in OpenTelemetry support through its ``ITelemetryService`` interface and ``DefaultTelemetryService`` implementation. Telemetry data is emitted automatically for key operations such as token issuance, client authentication, and request handling — with no additional configuration required to start collecting signals. + +Overview +-------- +OpenTelemetry defines three primary signal types: + +- **Metrics** — Numerical measurements that describe the behaviour of your system over time (e.g. request counts, error rates, active connections). +- **Traces** — Records of the path a request takes through your system, broken into spans that represent individual units of work. +- **Logs** — Timestamped text records, enriched with structured data, that capture discrete events. + +Open.IdentityServer emits metrics and traces natively using the .NET ``System.Diagnostics`` APIs (``Meter``, ``Counter``, ``ActivitySource``). Logging is handled through the standard ASP.NET Core ``ILogger`` infrastructure, which can be exported via OpenTelemetry's logging bridge. + +Getting Started +^^^^^^^^^^^^^^^ +OpenTelemetry signals are emitted by Open.IdentityServer automatically. To collect and export them, you need to configure the OpenTelemetry SDK in your host application. + +Install the required NuGet packages: + +.. code-block:: bash + + dotnet add package OpenTelemetry.Extensions.Hosting + dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol + +Then configure the OpenTelemetry SDK in your ``Program.cs`` or ``Startup.cs``: + +.. code-block:: csharp + + using OpenTelemetry.Metrics; + using OpenTelemetry.Trace; + using OpenTelemetry.Logs; + + var builder = WebApplication.CreateBuilder(args); + + builder.Services.AddIdentityServer() + // ... your IdentityServer configuration + ; + + builder.Services.AddOpenTelemetry() + .ConfigureResource(resource => + { + resource.AddService(""); + }) + .WithMetrics(metrics => + { + metrics + .AddMeter(TelemetryConstants.MetricsConstants.MeterName) + .AddOtlpExporter(); + }) + .WithTracing(tracing => + { + tracing + .AddSource(TelemetryConstants.TraceCategories.Basic) + .AddSource(TelemetryConstants.TraceCategories.Cache) + .AddSource(TelemetryConstants.TraceCategories.Services) + .AddSource(TelemetryConstants.TraceCategories.Stores) + .AddSource(TelemetryConstants.TraceCategories.Validation) + .AddOtlpExporter(); + }); + + builder.Logging.AddOpenTelemetry(logging => + { + logging.AddOtlpExporter(); + }); + +.. note:: + The ``DefaultTelemetryService`` is registered as a singleton automatically when you call ``AddIdentityServer()``. No additional registration is needed to enable telemetry. + +Metrics +------- +Open.IdentityServer exposes metrics through a .NET ``Meter`` named ``Open.IdentityServer``. All metric instruments are counters that track the volume and outcome of protocol operations. + +Meter Name +^^^^^^^^^^ +``Open.IdentityServer`` + +Available Instruments +^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 40 15 45 + + * - Instrument Name + - Type + - Description + * - ``tokenservice.operation`` + - Counter + - Counts all operations, tagged with ``result`` (``success``, ``error``, or ``internal_error``), ``client``, and optionally ``error``. + * - ``tokenservice.active_requests`` + - UpDownCounter + - Tracks the number of currently active IdentityServer protocol requests. Tagged with ``endpoint`` and ``path``. + * - ``tokenservice.api.secret_validation`` + - Counter + - Counts API secret validation attempts. Tagged with ``api``, ``auth_method`` (on success), and ``error`` (on failure). + * - ``tokenservice.client.secret_validation`` + - Counter + - Counts client secret validation attempts. Tagged with ``client``, ``auth_method`` (on success), and ``error`` (on failure). + * - ``tokenservice.client.config_validation`` + - Counter + - Counts client configuration validation attempts. Tagged with ``client`` and ``error`` (on failure). + * - ``tokenservice.token_issued`` + - Counter + - Counts tokens issued. Tagged with ``client``, ``grant_type``, and ``error`` (on failure). + * - ``tokenservice.introspection`` + - Counter + - Counts token introspection requests. Tagged with ``caller``, ``active`` (token status), and ``error`` (on failure). + * - ``tokenservice.revocation`` + - Counter + - Counts token revocation requests. Tagged with ``client`` and ``error`` (on failure). + * - ``tokenservice.device_authentication`` + - Counter + - Counts device authentication requests. Tagged with ``client`` and ``error`` (on failure). + * - ``tokenservice.resourceowner_authentication`` + - Counter + - Counts resource owner password authentication attempts. Tagged with ``client`` and ``error`` (on failure). + * - ``tokenservice.backchannel_authentication`` + - Counter + - Counts backchannel (CIBA) authentication requests. Tagged with ``client`` and ``error`` (on failure). **Note:** CIBA is not yet implemented in Open.IdentityServer, but the metric is reserved for future use. + * - ``tokenservice.pushed_authorization_request`` + - Counter + - Counts pushed authorization requests (PAR). Tagged with ``client`` and ``error`` (on failure). **Note:** PAR is not yet implemented in Open.IdentityServer, but the metric is reserved for future use. + +Metric Tags +^^^^^^^^^^^^ +Metrics are enriched with the following tags to enable filtering and grouping in your observability platform: + +.. list-table:: + :header-rows: 1 + :widths: 20 80 + + * - Tag + - Description + * - ``result`` + - The outcome of the operation: ``success``, ``error``, or ``internal_error``. + * - ``client`` + - The client identifier associated with the operation. + * - ``error`` + - A description of the error, present only when the operation failed. + * - ``endpoint`` + - The logical endpoint handling the request (e.g. ``TokenEndpoint``). + * - ``path`` + - The request path. + * - ``api`` + - The API resource name (used in API secret validation). + * - ``auth_method`` + - The authentication method used (e.g. ``client_secret_post``, ``private_key_jwt``). + * - ``caller`` + - The caller of the introspection endpoint. + * - ``active`` + - Whether the introspected token is active (``true``/``false``). + * - ``grant_type`` + - The OAuth grant type used for token issuance. + +Subscribing to Metrics +^^^^^^^^^^^^^^^^^^^^^^ +To collect Open.IdentityServer metrics, subscribe to the ``Open.IdentityServer`` meter in your OpenTelemetry configuration: + +.. code-block:: csharp + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddMeter(TelemetryConstants.MetricsConstants.MeterName); + }); + +Traces +------ +Open.IdentityServer uses .NET ``ActivitySource`` instances to create distributed trace spans. Traces provide visibility into the internal processing of each request, helping you identify performance bottlenecks and understand the flow of operations. + +Activity Sources +^^^^^^^^^^^^^^^^ +Open.IdentityServer defines several activity sources, each representing a category of operations: + +.. list-table:: + :header-rows: 1 + :widths: 40 60 + + * - Activity Source Name + - Description + * - ``Open.IdentityServer`` + - Top-level protocol request processing (e.g. the overall handling of a token or authorize request). + * - ``Open.IdentityServer.Cache`` + - Caching operations (cache reads, writes, and invalidations). + * - ``Open.IdentityServer.Services`` + - Internal service operations (token creation, consent, key material, etc.). + * - ``Open.IdentityServer.Stores`` + - Data store operations (reading/writing grants, clients, resources). + * - ``Open.IdentityServer.Validation`` + - Validation operations (token validation, request validation, secret validation). + +Subscribing to Traces +^^^^^^^^^^^^^^^^^^^^^ +To collect traces, add the relevant activity sources to your OpenTelemetry tracing configuration: + +.. code-block:: csharp + + builder.Services.AddOpenTelemetry() + .WithTracing(tracing => + { + // Subscribe to all Open.IdentityServer activity sources + tracing + .AddSource(TelemetryConstants.TraceCategories.Basic) + .AddSource(TelemetryConstants.TraceCategories.Cache) + .AddSource(TelemetryConstants.TraceCategories.Services) + .AddSource(TelemetryConstants.TraceCategories.Stores) + .AddSource(TelemetryConstants.TraceCategories.Validation); + }); + +You can subscribe to a subset of sources if you only need visibility into specific categories. For example, to trace only store operations: + +.. code-block:: csharp + + builder.Services.AddOpenTelemetry() + .WithTracing(tracing => + { + tracing.AddSource(TelemetryConstants.TraceCategories.Stores); + }); + +Trace Granularity +^^^^^^^^^^^^^^^^^ +Each trace activity is named after the class and method performing the operation, providing fine-grained visibility. For example, a token request might produce a trace hierarchy such as: + +:: + + Open.IdentityServer: IdentityServerProtocolRequest + └─ Open.IdentityServer: TokenEndpoint.ProcessAsync + └─ Open.IdentityServer.Validation: ClientSecretValidator.ValidateAsync + └─ Open.IdentityServer.Validation: TokenRequestValidator.ValidateRequestAsync + └─ Open.IdentityServer.Stores: DefaultRefreshTokenStore.GetRefreshTokenAsync + └─ Open.IdentityServer.Services: DefaultTokenService.CreateAccessTokenAsync + +Logging +------- +Open.IdentityServer uses the standard ASP.NET Core ``ILogger`` infrastructure for structured logging. Log messages are emitted at various levels throughout the processing pipeline, providing detailed diagnostic information. + +To export logs via OpenTelemetry, configure the logging bridge: + +.. code-block:: csharp + + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + logging.AddOtlpExporter(); + }); + +For more details on log levels and filtering, see the :ref:`Logging ` documentation page. + +.. note:: + If using Serilog, you will need to use the `OpenTelemetry Serilog Sink `_ to forward logs to OpenTelemetry. The built-in logging bridge does not support Serilog directly. + +Exporters +--------- +OpenTelemetry supports a wide variety of exporters for sending telemetry data to your chosen backend. Common choices include: + +- **OTLP (OpenTelemetry Protocol)** — The standard protocol, supported by most observability platforms (e.g. Jaeger, Grafana, Datadog, Azure Monitor, AWS X-Ray). +- **Prometheus** — For metrics scraping (use ``OpenTelemetry.Exporter.Prometheus.AspNetCore``). +- **Console** — Useful during development (use ``OpenTelemetry.Exporter.Console``). + +Example using the Console exporter for development: + +.. code-block:: csharp + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics + .AddMeter("Open.IdentityServer") + .AddConsoleExporter(); + }) + .WithTracing(tracing => + { + tracing + .AddSource("Open.IdentityServer") + .AddSource("Open.IdentityServer.Cache") + .AddSource("Open.IdentityServer.Services") + .AddSource("Open.IdentityServer.Stores") + .AddSource("Open.IdentityServer.Validation") + .AddConsoleExporter(); + }); + +Customisation +------------- +The ``ITelemetryService`` interface can be replaced with a custom implementation if you need to modify or extend the telemetry behaviour. Register your custom implementation before calling ``AddIdentityServer()``: + +.. code-block:: csharp + + builder.Services.AddSingleton(); + + builder.Services.AddIdentityServer() + // ... + ; + +Since the default implementation is registered with ``TryAddSingleton``, any prior registration will take precedence. + +.. note:: + In most cases, the default implementation combined with OpenTelemetry SDK configuration provides sufficient flexibility without needing a custom ``ITelemetryService``. diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 38e8f1d55..3a9cae0ba 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -5,7 +5,7 @@ 10.0.5 10.0.5 - 1.0.1-* + 1.0.2-* diff --git a/src/EntityFramework.Storage/src/Stores/ClientStore.cs b/src/EntityFramework.Storage/src/Stores/ClientStore.cs index c611c6c01..5f2362117 100644 --- a/src/EntityFramework.Storage/src/Stores/ClientStore.cs +++ b/src/EntityFramework.Storage/src/Stores/ClientStore.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -11,6 +12,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Open.IdentityServer.EntityFramework.Mappers; +using Open.IdentityServer.Services; namespace Open.IdentityServer.EntityFramework.Stores; @@ -25,6 +27,11 @@ public class ClientStore : IClientStore /// protected readonly IConfigurationDbContext Context; + /// + /// The telemetry. + /// + protected readonly ITelemetryService Telemetry; + /// /// The logger. /// @@ -34,11 +41,13 @@ public class ClientStore : IClientStore /// Initializes a new instance of the class. /// /// The context. + /// The telemetry service. /// The logger. /// context - public ClientStore(IConfigurationDbContext context, ILogger logger) + public ClientStore(IConfigurationDbContext context, ITelemetryService telemetry, ILogger logger) { Context = context ?? throw new ArgumentNullException(nameof(context)); + Telemetry = telemetry; Logger = logger; } @@ -51,6 +60,9 @@ public ClientStore(IConfigurationDbContext context, ILogger logger) /// public virtual async Task FindClientByIdAsync(string clientId) { + using var trace = Telemetry + .Trace(TelemetryConstants.TraceCategories.Stores, this); + IQueryable baseQuery = Context.Clients .Where(x => x.ClientId == clientId); diff --git a/src/EntityFramework.Storage/src/Stores/DeviceFlowStore.cs b/src/EntityFramework.Storage/src/Stores/DeviceFlowStore.cs index 1e231ca85..79366a3b9 100644 --- a/src/EntityFramework.Storage/src/Stores/DeviceFlowStore.cs +++ b/src/EntityFramework.Storage/src/Stores/DeviceFlowStore.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -12,6 +13,7 @@ using Open.IdentityServer.Stores.Serialization; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using Open.IdentityServer.Services; namespace Open.IdentityServer.EntityFramework.Stores; @@ -31,6 +33,11 @@ public class DeviceFlowStore : IDeviceFlowStore /// protected readonly IPersistentGrantSerializer Serializer; + /// + /// The telemetry service. + /// + protected readonly ITelemetryService Telemetry; + /// /// The logger. /// @@ -40,15 +47,18 @@ public class DeviceFlowStore : IDeviceFlowStore /// Initializes a new instance of the class. /// /// The context. - /// The serializer + /// The serializer. + /// The telemetry service. /// The logger. public DeviceFlowStore( IPersistedGrantDbContext context, - IPersistentGrantSerializer serializer, + IPersistentGrantSerializer serializer, + ITelemetryService telemetry, ILogger logger) { Context = context; Serializer = serializer; + Telemetry = telemetry; Logger = logger; } @@ -60,6 +70,8 @@ public DeviceFlowStore( /// The data. public virtual async Task StoreDeviceAuthorizationAsync(string deviceCode, string userCode, DeviceCode data) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Stores, this); + Context.DeviceFlowCodes.Add(ToEntity(data, deviceCode, userCode)); await Context.SaveChangesAsync(); @@ -75,6 +87,8 @@ public virtual async Task StoreDeviceAuthorizationAsync(string deviceCode, strin /// public virtual async Task FindByUserCodeAsync(string userCode) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Stores, this); + var deviceFlowCodes = (await Context.DeviceFlowCodes.AsNoTracking().Where(x => x.UserCode == userCode).ToArrayAsync()) .SingleOrDefault(x => x.UserCode == userCode); var model = ToModel(deviceFlowCodes?.Data); @@ -94,6 +108,8 @@ public virtual async Task FindByUserCodeAsync(string userCode) /// public virtual async Task FindByDeviceCodeAsync(string deviceCode) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Stores, this); + var deviceFlowCodes = (await Context.DeviceFlowCodes.AsNoTracking().Where(x => x.DeviceCode == deviceCode).ToArrayAsync()) .SingleOrDefault(x => x.DeviceCode == deviceCode); var model = ToModel(deviceFlowCodes?.Data); @@ -110,6 +126,8 @@ public virtual async Task FindByDeviceCodeAsync(string deviceCode) /// The data. public virtual async Task UpdateByUserCodeAsync(string userCode, DeviceCode data) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Stores, this); + var existing = (await Context.DeviceFlowCodes.Where(x => x.UserCode == userCode).ToArrayAsync()) .SingleOrDefault(x => x.UserCode == userCode); if (existing == null) @@ -140,6 +158,8 @@ public virtual async Task UpdateByUserCodeAsync(string userCode, DeviceCode data /// The device code. public virtual async Task RemoveByDeviceCodeAsync(string deviceCode) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Stores, this); + var deviceFlowCodes = (await Context.DeviceFlowCodes.Where(x => x.DeviceCode == deviceCode).ToArrayAsync()) .SingleOrDefault(x => x.DeviceCode == deviceCode); diff --git a/src/EntityFramework.Storage/src/Stores/PersistedGrantStore.cs b/src/EntityFramework.Storage/src/Stores/PersistedGrantStore.cs index 91a81edc0..f9bd48eb9 100644 --- a/src/EntityFramework.Storage/src/Stores/PersistedGrantStore.cs +++ b/src/EntityFramework.Storage/src/Stores/PersistedGrantStore.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -13,6 +14,7 @@ using System; using Open.IdentityServer.EntityFramework.Mappers; using Open.IdentityServer.Extensions; +using Open.IdentityServer.Services; namespace Open.IdentityServer.EntityFramework.Stores; @@ -27,6 +29,11 @@ public class PersistedGrantStore : IPersistedGrantStore /// protected readonly IPersistedGrantDbContext Context; + /// + /// The telemetry service. + /// + protected readonly ITelemetryService Telemetry; + /// /// The logger. /// @@ -36,16 +43,20 @@ public class PersistedGrantStore : IPersistedGrantStore /// Initializes a new instance of the class. /// /// The context. + /// The telemetry service. /// The logger. - public PersistedGrantStore(IPersistedGrantDbContext context, ILogger logger) + public PersistedGrantStore(IPersistedGrantDbContext context, ITelemetryService telemetry, ILogger logger) { Context = context; + Telemetry = telemetry; Logger = logger; } /// public virtual async Task StoreAsync(PersistedGrant token) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Stores, this); + var existing = (await Context.PersistedGrants.Where(x => x.Key == token.Key).ToArrayAsync()) .SingleOrDefault(x => x.Key == token.Key); if (existing == null) @@ -75,6 +86,8 @@ public virtual async Task StoreAsync(PersistedGrant token) /// public virtual async Task GetAsync(string key) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Stores, this); + var persistedGrant = (await Context.PersistedGrants.AsNoTracking().Where(x => x.Key == key).ToArrayAsync()) .SingleOrDefault(x => x.Key == key); var model = persistedGrant?.ToModel(); @@ -87,6 +100,8 @@ public virtual async Task GetAsync(string key) /// public async Task> GetAllAsync(PersistedGrantFilter filter) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Stores, this); + filter.Validate(); var persistedGrants = await Filter(Context.PersistedGrants.AsQueryable(), filter).ToArrayAsync(); @@ -102,6 +117,8 @@ public async Task> GetAllAsync(PersistedGrantFilter /// public virtual async Task RemoveAsync(string key) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Stores, this); + var persistedGrant = (await Context.PersistedGrants.Where(x => x.Key == key).ToArrayAsync()) .SingleOrDefault(x => x.Key == key); if (persistedGrant!= null) @@ -128,6 +145,8 @@ public virtual async Task RemoveAsync(string key) /// public async Task RemoveAllAsync(PersistedGrantFilter filter) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Stores, this); + filter.Validate(); var persistedGrants = await Filter(Context.PersistedGrants.AsQueryable(), filter).ToArrayAsync(); diff --git a/src/EntityFramework.Storage/src/Stores/ResourceStore.cs b/src/EntityFramework.Storage/src/Stores/ResourceStore.cs index f3b5388d3..faba5d09d 100644 --- a/src/EntityFramework.Storage/src/Stores/ResourceStore.cs +++ b/src/EntityFramework.Storage/src/Stores/ResourceStore.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -12,6 +13,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Open.IdentityServer.EntityFramework.Mappers; +using Open.IdentityServer.Services; namespace Open.IdentityServer.EntityFramework.Stores; @@ -25,21 +27,29 @@ public class ResourceStore : IResourceStore /// The DbContext. /// protected readonly IConfigurationDbContext Context; - + + /// + /// The Telemetry service + /// + protected readonly ITelemetryService Telemetry; + /// /// The logger. /// protected readonly ILogger Logger; + /// /// Initializes a new instance of the class. /// /// The context. + /// The telemetry service /// The logger. /// context - public ResourceStore(IConfigurationDbContext context, ILogger logger) + public ResourceStore(IConfigurationDbContext context, ITelemetryService telemetry, ILogger logger) { Context = context ?? throw new ArgumentNullException(nameof(context)); + Telemetry = telemetry ?? throw new ArgumentNullException(nameof(telemetry)); Logger = logger; } @@ -51,6 +61,7 @@ public ResourceStore(IConfigurationDbContext context, ILogger log public virtual async Task> FindApiResourcesByNameAsync(IEnumerable apiResourceNames) { if (apiResourceNames == null) throw new ArgumentNullException(nameof(apiResourceNames)); + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Stores, this); var query = from apiResource in Context.ApiResources @@ -87,6 +98,8 @@ where apiResourceNames.Contains(apiResource.Name) /// The models matching any of the ; empty when none are found. public virtual async Task> FindApiResourcesByScopeNameAsync(IEnumerable scopeNames) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Stores, this); + var names = scopeNames.ToArray(); var query = @@ -117,6 +130,8 @@ where api.Scopes.Where(x => names.Contains(x.Scope)).Any() /// The models whose name matches an entry in ; empty when none match. public virtual async Task> FindIdentityResourcesByScopeNameAsync(IEnumerable scopeNames) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Stores, this); + var scopes = scopeNames.ToArray(); var query = @@ -144,6 +159,8 @@ where scopes.Contains(identityResource.Name) /// The models whose name matches an entry in ; empty when none match. public virtual async Task> FindApiScopesByNameAsync(IEnumerable scopeNames) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Stores, this); + var scopes = scopeNames.ToArray(); var query = @@ -170,6 +187,8 @@ where scopes.Contains(scope.Name) /// A aggregate containing every identity resource, API resource, and API scope currently persisted in the configuration store. public virtual async Task GetAllResourcesAsync() { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Stores, this); + var identity = Context.IdentityResources .Include(x => x.UserClaims) .Include(x => x.Properties); diff --git a/src/EntityFramework.Storage/test/IntegrationTests/Open.IdentityServer.EntityFramework.IntegrationTests.csproj b/src/EntityFramework.Storage/test/IntegrationTests/Open.IdentityServer.EntityFramework.IntegrationTests.csproj index 60534d618..434758918 100644 --- a/src/EntityFramework.Storage/test/IntegrationTests/Open.IdentityServer.EntityFramework.IntegrationTests.csproj +++ b/src/EntityFramework.Storage/test/IntegrationTests/Open.IdentityServer.EntityFramework.IntegrationTests.csproj @@ -25,6 +25,7 @@ + diff --git a/src/EntityFramework.Storage/test/IntegrationTests/Stores/ClientStoreTests.cs b/src/EntityFramework.Storage/test/IntegrationTests/Stores/ClientStoreTests.cs index ae6051a33..4a76a8ce1 100644 --- a/src/EntityFramework.Storage/test/IntegrationTests/Stores/ClientStoreTests.cs +++ b/src/EntityFramework.Storage/test/IntegrationTests/Stores/ClientStoreTests.cs @@ -4,6 +4,8 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using AwesomeAssertions; using Open.IdentityServer.EntityFramework.DbContexts; @@ -11,7 +13,9 @@ using Open.IdentityServer.EntityFramework.Stores; using Open.IdentityServer.Models; using Microsoft.EntityFrameworkCore; +using Moq; using Open.IdentityServer.EntityFramework.Mappers; +using Open.IdentityServer.Services; using Xunit; using Xunit.Sdk; @@ -19,6 +23,8 @@ namespace Open.IdentityServer.EntityFramework.IntegrationTests.Stores; public class ClientStoreTests : IntegrationTest { + private ITelemetryService _telemetry = Mock.Of(); + public ClientStoreTests(DatabaseProviderFixture fixture) : base(fixture) { foreach (var row in TestDatabaseProviders) @@ -27,13 +33,18 @@ public ClientStoreTests(DatabaseProviderFixture fixture) context.Database.EnsureCreated(); } } + + private ClientStore CreateStore(ConfigurationDbContext context) + { + return new ClientStore(context, _telemetry, FakeLogger.Create()); + } [Theory, MemberData(nameof(TestDatabaseProviders))] public async Task FindClientByIdAsync_WhenClientDoesNotExist_ExpectNull( DbContextOptions options) { await using var context = new ConfigurationDbContext(options, StoreOptions); - var store = new ClientStore(context, FakeLogger.Create()); + var store = CreateStore(context); var client = await store.FindClientByIdAsync(Guid.NewGuid().ToString()); client.Should().BeNull(); } @@ -57,7 +68,7 @@ public async Task FindClientByIdAsync_WhenClientExists_ExpectClientRetured( Client client; await using (var context = new ConfigurationDbContext(options, StoreOptions)) { - var store = new ClientStore(context, FakeLogger.Create()); + var store = CreateStore(context); client = await store.FindClientByIdAsync(testClient.ClientId); } @@ -92,7 +103,7 @@ public async Task FindClientByIdAsync_WhenClientExistsWithCollections_ExpectClie Client client; await using (var context = new ConfigurationDbContext(options, StoreOptions)) { - var store = new ClientStore(context, FakeLogger.Create()); + var store = CreateStore(context); client = await store.FindClientByIdAsync(testClient.ClientId); } @@ -141,7 +152,8 @@ public async Task FindClientByIdAsync_WhenClientsExistWithManyCollections_Expect await using (var context = new ConfigurationDbContext(options, StoreOptions)) { - var store = new ClientStore(context, FakeLogger.Create()); + var clientStore = CreateStore(context); + var store = clientStore; const int timeout = 5000; var task = Task.Run(() => store.FindClientByIdAsync(testClient.ClientId)); @@ -157,4 +169,43 @@ public async Task FindClientByIdAsync_WhenClientsExistWithManyCollections_Expect } } } + + [Theory, MemberData(nameof(TestDatabaseProviders))] + public async Task PublicMethods_WhenCalled_ShouldTelemetryTrace(DbContextOptions options) + { + List<(Func actMethod, string traceMethodName)> methods + = new() + { + (store => store.FindClientByIdAsync("clientId"), "FindClientByIdAsync"), + }; + + foreach (var method in methods) + { + var trace = Mock.Of(); + Mock.Get(_telemetry).Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(trace); + Mock.Get(trace).Setup(t => t.AddTag(It.IsAny(), It.IsAny())).Returns(trace); + Mock.Get(trace).Setup(t => t.AddTag(It.IsAny(), It.IsAny())).Returns(trace); + + using (var context = new ConfigurationDbContext(options, StoreOptions)) + { + var store = CreateStore(context); + + await method.actMethod(store); + + Mock.Get(_telemetry) + .Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Stores, store, method.traceMethodName), Times.Once); + Mock.Get(trace).Verify(t => t.Dispose(), Times.Once); + } + } + + // Assert all methods covered + typeof(ClientStore).GetMethods() + .Where(m => m.IsPublic && !m.IsStatic && !m.IsSpecialName) + .Where(m => m.DeclaringType == typeof(ClientStore)) + .Select(m => m.Name) + .Distinct() + .Should().BeEquivalentTo(methods.Select(m => m.traceMethodName)); + } } \ No newline at end of file diff --git a/src/EntityFramework.Storage/test/IntegrationTests/Stores/DeviceFlowStoreTests.cs b/src/EntityFramework.Storage/test/IntegrationTests/Stores/DeviceFlowStoreTests.cs index e13884af9..1cdfc3a11 100644 --- a/src/EntityFramework.Storage/test/IntegrationTests/Stores/DeviceFlowStoreTests.cs +++ b/src/EntityFramework.Storage/test/IntegrationTests/Stores/DeviceFlowStoreTests.cs @@ -17,22 +17,40 @@ using System.Threading.Tasks; using Open.IdentityServer.EntityFramework.Entities; using Microsoft.EntityFrameworkCore.InMemory.Infrastructure.Internal; +using Moq; +using Open.IdentityServer.Services; using Xunit; namespace Open.IdentityServer.EntityFramework.IntegrationTests.Stores; public class DeviceFlowStoreTests : IntegrationTest { - private readonly IPersistentGrantSerializer serializer = new PersistentGrantSerializer(); + private readonly IPersistentGrantSerializer _serializer = new PersistentGrantSerializer(); + private readonly ITelemetryService _telemetry = Mock.Of(); - public DeviceFlowStoreTests(DatabaseProviderFixture fixture) : base(fixture) + public DeviceFlowStoreTests(DatabaseProviderFixture fixture) : base(fixture) + { + foreach (var row in TestDatabaseProviders) { - foreach (var row in TestDatabaseProviders) - { - using var context = new PersistedGrantDbContext(row.Data, StoreOptions); - context.Database.EnsureCreated(); - } + using var context = new PersistedGrantDbContext(row.Data, StoreOptions); + context.Database.EnsureCreated(); } + } + + private DeviceFlowStore CreateStore(PersistedGrantDbContext context) + { + return new DeviceFlowStore(context, new PersistentGrantSerializer(), _telemetry, FakeLogger.Create()); + } + + private DeviceCode CreateTestDeviceCode() + { + return new DeviceCode + { + ClientId = Guid.NewGuid().ToString(), + CreationTime = DateTime.UtcNow, + Lifetime = 300 + }; + } [Theory, MemberData(nameof(TestDatabaseProviders))] public async Task StoreDeviceAuthorizationAsync_WhenSuccessful_ExpectDeviceCodeAndUserCodeStored(DbContextOptions options) @@ -48,7 +66,7 @@ public async Task StoreDeviceAuthorizationAsync_WhenSuccessful_ExpectDeviceCodeA await using (var context = new PersistedGrantDbContext(options, StoreOptions)) { - var store = new DeviceFlowStore(context, new PersistentGrantSerializer(), FakeLogger.Create()); + var store = CreateStore(context); await store.StoreDeviceAuthorizationAsync(deviceCode, userCode, data); } @@ -76,7 +94,7 @@ public async Task StoreDeviceAuthorizationAsync_WhenSuccessful_ExpectDataStored( await using (var context = new PersistedGrantDbContext(options, StoreOptions)) { - var store = new DeviceFlowStore(context, new PersistentGrantSerializer(), FakeLogger.Create()); + var store = CreateStore(context); await store.StoreDeviceAuthorizationAsync(deviceCode, userCode, data); } @@ -119,14 +137,14 @@ public async Task StoreDeviceAuthorizationAsync_WhenUserCodeAlreadyExists_Expect SubjectId = deviceCodeData.Subject.FindFirst(JwtClaimTypes.Subject).Value, CreationTime = deviceCodeData.CreationTime, Expiration = deviceCodeData.CreationTime.AddSeconds(deviceCodeData.Lifetime), - Data = serializer.Serialize(deviceCodeData) + Data = _serializer.Serialize(deviceCodeData) }); await context.SaveChangesAsync(); } await using (var context = new PersistedGrantDbContext(options, StoreOptions)) { - var store = new DeviceFlowStore(context, new PersistentGrantSerializer(), FakeLogger.Create()); + var store = CreateStore(context); // skip odd behaviour of in-memory provider if (options.Extensions.All(x => x.GetType() != typeof(InMemoryOptionsExtension))) @@ -163,14 +181,14 @@ public async Task StoreDeviceAuthorizationAsync_WhenDeviceCodeAlreadyExists_Expe SubjectId = deviceCodeData.Subject.FindFirst(JwtClaimTypes.Subject).Value, CreationTime = deviceCodeData.CreationTime, Expiration = deviceCodeData.CreationTime.AddSeconds(deviceCodeData.Lifetime), - Data = serializer.Serialize(deviceCodeData) + Data = _serializer.Serialize(deviceCodeData) }); await context.SaveChangesAsync(); } await using (var context = new PersistedGrantDbContext(options, StoreOptions)) { - var store = new DeviceFlowStore(context, new PersistentGrantSerializer(), FakeLogger.Create()); + var store = CreateStore(context); // skip odd behaviour of in-memory provider if (options.Extensions.All(x => x.GetType() != typeof(InMemoryOptionsExtension))) @@ -208,7 +226,7 @@ public async Task FindByUserCodeAsync_WhenUserCodeExists_ExpectDataRetrievedCorr SubjectId = expectedDeviceCodeData.Subject.FindFirst(JwtClaimTypes.Subject).Value, CreationTime = expectedDeviceCodeData.CreationTime, Expiration = expectedDeviceCodeData.CreationTime.AddSeconds(expectedDeviceCodeData.Lifetime), - Data = serializer.Serialize(expectedDeviceCodeData) + Data = _serializer.Serialize(expectedDeviceCodeData) }); await context.SaveChangesAsync(); } @@ -216,7 +234,7 @@ public async Task FindByUserCodeAsync_WhenUserCodeExists_ExpectDataRetrievedCorr DeviceCode code; await using (var context = new PersistedGrantDbContext(options, StoreOptions)) { - var store = new DeviceFlowStore(context, new PersistentGrantSerializer(), FakeLogger.Create()); + var store = CreateStore(context); code = await store.FindByUserCodeAsync(testUserCode); } @@ -230,7 +248,7 @@ public async Task FindByUserCodeAsync_WhenUserCodeExists_ExpectDataRetrievedCorr public async Task FindByUserCodeAsync_WhenUserCodeDoesNotExist_ExpectNull(DbContextOptions options) { await using var context = new PersistedGrantDbContext(options, StoreOptions); - var store = new DeviceFlowStore(context, new PersistentGrantSerializer(), FakeLogger.Create()); + var store = CreateStore(context); var code = await store.FindByUserCodeAsync($"user_{Guid.NewGuid().ToString()}"); code.Should().BeNull(); } @@ -262,7 +280,7 @@ public async Task FindByDeviceCodeAsync_WhenDeviceCodeExists_ExpectDataRetrieved SubjectId = expectedDeviceCodeData.Subject.FindFirst(JwtClaimTypes.Subject).Value, CreationTime = expectedDeviceCodeData.CreationTime, Expiration = expectedDeviceCodeData.CreationTime.AddSeconds(expectedDeviceCodeData.Lifetime), - Data = serializer.Serialize(expectedDeviceCodeData) + Data = _serializer.Serialize(expectedDeviceCodeData) }); await context.SaveChangesAsync(); } @@ -270,7 +288,7 @@ public async Task FindByDeviceCodeAsync_WhenDeviceCodeExists_ExpectDataRetrieved DeviceCode code; await using (var context = new PersistedGrantDbContext(options, StoreOptions)) { - var store = new DeviceFlowStore(context, new PersistentGrantSerializer(), FakeLogger.Create()); + var store = CreateStore(context); code = await store.FindByDeviceCodeAsync(testDeviceCode); } @@ -284,7 +302,7 @@ public async Task FindByDeviceCodeAsync_WhenDeviceCodeExists_ExpectDataRetrieved public async Task FindByDeviceCodeAsync_WhenDeviceCodeDoesNotExist_ExpectNull(DbContextOptions options) { await using var context = new PersistedGrantDbContext(options, StoreOptions); - var store = new DeviceFlowStore(context, new PersistentGrantSerializer(), FakeLogger.Create()); + var store = CreateStore(context); var code = await store.FindByDeviceCodeAsync($"device_{Guid.NewGuid().ToString()}"); code.Should().BeNull(); } @@ -314,7 +332,7 @@ public async Task UpdateByUserCodeAsync_WhenDeviceCodeAuthorized_ExpectSubjectAn ClientId = unauthorizedDeviceCode.ClientId, CreationTime = unauthorizedDeviceCode.CreationTime, Expiration = unauthorizedDeviceCode.CreationTime.AddSeconds(unauthorizedDeviceCode.Lifetime), - Data = serializer.Serialize(unauthorizedDeviceCode) + Data = _serializer.Serialize(unauthorizedDeviceCode) }); await context.SaveChangesAsync(); } @@ -333,7 +351,7 @@ public async Task UpdateByUserCodeAsync_WhenDeviceCodeAuthorized_ExpectSubjectAn await using (var context = new PersistedGrantDbContext(options, StoreOptions)) { - var store = new DeviceFlowStore(context, new PersistentGrantSerializer(), FakeLogger.Create()); + var store = CreateStore(context); await store.UpdateByUserCodeAsync(testUserCode, authorizedDeviceCode); } @@ -350,7 +368,7 @@ public async Task UpdateByUserCodeAsync_WhenDeviceCodeAuthorized_ExpectSubjectAn updatedCodes.Expiration.Should().Be(unauthorizedDeviceCode.CreationTime.AddSeconds(authorizedDeviceCode.Lifetime)); // should be changed - var parsedCode = serializer.Deserialize(updatedCodes.Data); + var parsedCode = _serializer.Deserialize(updatedCodes.Data); parsedCode.Should().BeEquivalentTo(authorizedDeviceCode, assertionOptions => assertionOptions.Excluding(x => x.Subject)); parsedCode.Subject.Claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Subject && x.Value == expectedSubject).Should().NotBeNull(); } @@ -379,14 +397,14 @@ public async Task RemoveByDeviceCodeAsync_WhenDeviceCodeExists_ExpectDeviceCodeD ClientId = existingDeviceCode.ClientId, CreationTime = existingDeviceCode.CreationTime, Expiration = existingDeviceCode.CreationTime.AddSeconds(existingDeviceCode.Lifetime), - Data = serializer.Serialize(existingDeviceCode) + Data = _serializer.Serialize(existingDeviceCode) }); await context.SaveChangesAsync(); } await using (var context = new PersistedGrantDbContext(options, StoreOptions)) { - var store = new DeviceFlowStore(context, new PersistentGrantSerializer(), FakeLogger.Create()); + var store = CreateStore(context); await store.RemoveByDeviceCodeAsync(testDeviceCode); } @@ -400,7 +418,51 @@ public async Task RemoveByDeviceCodeAsync_WhenDeviceCodeExists_ExpectDeviceCodeD public async Task RemoveByDeviceCodeAsync_WhenDeviceCodeDoesNotExists_ExpectSuccess(DbContextOptions options) { await using var context = new PersistedGrantDbContext(options, StoreOptions); - var store = new DeviceFlowStore(context, new PersistentGrantSerializer(), FakeLogger.Create()); + var store = CreateStore(context); await store.RemoveByDeviceCodeAsync($"device_{Guid.NewGuid().ToString()}"); } + + [Theory, MemberData(nameof(TestDatabaseProviders))] + public async Task PublicMethods_WhenCalled_ShouldTelemetryTrace(DbContextOptions options) + { + List<(Func actMethod, string traceMethodName)> methods + = new() + { + (store => store.StoreDeviceAuthorizationAsync("deviceCode", "userCode", CreateTestDeviceCode()), "StoreDeviceAuthorizationAsync"), + (store => store.FindByUserCodeAsync("userCode"), "FindByUserCodeAsync"), + (store => store.FindByDeviceCodeAsync("deviceCode"), "FindByDeviceCodeAsync"), + (store => store.UpdateByUserCodeAsync("userCode", CreateTestDeviceCode()), "UpdateByUserCodeAsync"), + (store => store.RemoveByDeviceCodeAsync("deviceCode"), "RemoveByDeviceCodeAsync") + }; + + foreach (var method in methods) + { + var trace = Mock.Of(); + Mock.Get(_telemetry).Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(trace); + Mock.Get(trace).Setup(t => t.AddTag(It.IsAny(), It.IsAny())).Returns(trace); + Mock.Get(trace).Setup(t => t.AddTag(It.IsAny(), It.IsAny())).Returns(trace); + + using (var context = new PersistedGrantDbContext(options, StoreOptions)) + { + var store = CreateStore(context); + + await method.actMethod(store); + + Mock.Get(_telemetry) + .Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Stores, store, method.traceMethodName), Times.Once); + Mock.Get(trace) + .Verify(t => t.Dispose(), Times.Once); + } + } + + // Assert all methods covered + typeof(DeviceFlowStore).GetMethods() + .Where(m => m.IsPublic && !m.IsStatic && !m.IsSpecialName) + .Where(m => m.DeclaringType == typeof(DeviceFlowStore)) + .Select(m => m.Name) + .Distinct() + .Should().BeEquivalentTo(methods.Select(m => m.traceMethodName)); + } } \ No newline at end of file diff --git a/src/EntityFramework.Storage/test/IntegrationTests/Stores/PersistedGrantStoreTests.cs b/src/EntityFramework.Storage/test/IntegrationTests/Stores/PersistedGrantStoreTests.cs index 905fd0843..6dd5eec44 100644 --- a/src/EntityFramework.Storage/test/IntegrationTests/Stores/PersistedGrantStoreTests.cs +++ b/src/EntityFramework.Storage/test/IntegrationTests/Stores/PersistedGrantStoreTests.cs @@ -14,13 +14,17 @@ using Open.IdentityServer.Models; using Open.IdentityServer.Stores; using Microsoft.EntityFrameworkCore; +using Moq; using Open.IdentityServer.EntityFramework.Mappers; +using Open.IdentityServer.Services; using Xunit; namespace Open.IdentityServer.EntityFramework.IntegrationTests.Stores; public class PersistedGrantStoreTests : IntegrationTest { + private readonly ITelemetryService _telemetry = Mock.Of(); + public PersistedGrantStoreTests(DatabaseProviderFixture fixture) : base(fixture) { foreach (var row in TestDatabaseProviders) @@ -52,7 +56,7 @@ public async Task StoreAsync_WhenPersistedGrantStored_ExpectSuccess(DbContextOpt await using (var context = new PersistedGrantDbContext(options, StoreOptions)) { - var store = new PersistedGrantStore(context, FakeLogger.Create()); + var store = new PersistedGrantStore(context, _telemetry, FakeLogger.Create()); await store.StoreAsync(persistedGrant); } @@ -77,7 +81,7 @@ public async Task GetAsync_WithKeyAndPersistedGrantExists_ExpectPersistedGrantRe PersistedGrant foundPersistedGrant; await using (var context = new PersistedGrantDbContext(options, StoreOptions)) { - var store = new PersistedGrantStore(context, FakeLogger.Create()); + var store = new PersistedGrantStore(context, _telemetry, FakeLogger.Create()); foundPersistedGrant = await store.GetAsync(persistedGrant.Key); } @@ -98,7 +102,7 @@ public async Task GetAllAsync_WithSubAndTypeAndPersistedGrantExists_ExpectPersis IList foundPersistedGrants; await using (var context = new PersistedGrantDbContext(options, StoreOptions)) { - var store = new PersistedGrantStore(context, FakeLogger.Create()); + var store = new PersistedGrantStore(context, _telemetry, FakeLogger.Create()); foundPersistedGrants = (await store.GetAllAsync(new PersistedGrantFilter { SubjectId = persistedGrant.SubjectId })).ToList(); } @@ -127,7 +131,7 @@ public async Task GetAllAsync_Should_Filter(DbContextOptions.Create()); + var store = new PersistedGrantStore(context, _telemetry, FakeLogger.Create()); (await store.GetAllAsync(new PersistedGrantFilter { @@ -199,7 +203,7 @@ public async Task RemoveAsync_WhenKeyOfExistingReceived_ExpectGrantDeleted(DbCon await using (var context = new PersistedGrantDbContext(options, StoreOptions)) { - var store = new PersistedGrantStore(context, FakeLogger.Create()); + var store = new PersistedGrantStore(context, _telemetry, FakeLogger.Create()); await store.RemoveAsync(persistedGrant.Key); } @@ -223,7 +227,7 @@ public async Task RemoveAllAsync_WhenSubIdAndClientIdOfExistingReceived_ExpectGr await using (var context = new PersistedGrantDbContext(options, StoreOptions)) { - var store = new PersistedGrantStore(context, FakeLogger.Create()); + var store = new PersistedGrantStore(context, _telemetry, FakeLogger.Create()); await store.RemoveAllAsync(new PersistedGrantFilter { SubjectId = persistedGrant.SubjectId, ClientId = persistedGrant.ClientId @@ -250,7 +254,7 @@ public async Task RemoveAllAsync_WhenSubIdClientIdAndTypeOfExistingReceived_Expe await using (var context = new PersistedGrantDbContext(options, StoreOptions)) { - var store = new PersistedGrantStore(context, FakeLogger.Create()); + var store = new PersistedGrantStore(context, _telemetry, FakeLogger.Create()); await store.RemoveAllAsync(new PersistedGrantFilter { SubjectId = persistedGrant.SubjectId, ClientId = persistedGrant.ClientId, @@ -271,7 +275,7 @@ public async Task RemoveAllAsync_Should_Filter(DbContextOptions.Create()); + var store = new PersistedGrantStore(context, _telemetry, FakeLogger.Create()); await store.RemoveAllAsync(new PersistedGrantFilter { @@ -283,7 +287,7 @@ await store.RemoveAllAsync(new PersistedGrantFilter await PopulateDb(); await using (var context = new PersistedGrantDbContext(options, StoreOptions)) { - var store = new PersistedGrantStore(context, FakeLogger.Create()); + var store = new PersistedGrantStore(context, _telemetry, FakeLogger.Create()); await store.RemoveAllAsync(new PersistedGrantFilter { @@ -295,7 +299,7 @@ await store.RemoveAllAsync(new PersistedGrantFilter await PopulateDb(); await using (var context = new PersistedGrantDbContext(options, StoreOptions)) { - var store = new PersistedGrantStore(context, FakeLogger.Create()); + var store = new PersistedGrantStore(context, _telemetry, FakeLogger.Create()); await store.RemoveAllAsync(new PersistedGrantFilter { @@ -307,7 +311,7 @@ await store.RemoveAllAsync(new PersistedGrantFilter await PopulateDb(); await using (var context = new PersistedGrantDbContext(options, StoreOptions)) { - var store = new PersistedGrantStore(context, FakeLogger.Create()); + var store = new PersistedGrantStore(context, _telemetry, FakeLogger.Create()); await store.RemoveAllAsync(new PersistedGrantFilter { @@ -320,7 +324,7 @@ await store.RemoveAllAsync(new PersistedGrantFilter await PopulateDb(); await using (var context = new PersistedGrantDbContext(options, StoreOptions)) { - var store = new PersistedGrantStore(context, FakeLogger.Create()); + var store = new PersistedGrantStore(context, _telemetry, FakeLogger.Create()); await store.RemoveAllAsync(new PersistedGrantFilter { @@ -334,7 +338,7 @@ await store.RemoveAllAsync(new PersistedGrantFilter await PopulateDb(); await using (var context = new PersistedGrantDbContext(options, StoreOptions)) { - var store = new PersistedGrantStore(context, FakeLogger.Create()); + var store = new PersistedGrantStore(context, _telemetry, FakeLogger.Create()); await store.RemoveAllAsync(new PersistedGrantFilter { @@ -347,7 +351,7 @@ await store.RemoveAllAsync(new PersistedGrantFilter await PopulateDb(); await using (var context = new PersistedGrantDbContext(options, StoreOptions)) { - var store = new PersistedGrantStore(context, FakeLogger.Create()); + var store = new PersistedGrantStore(context, _telemetry, FakeLogger.Create()); await store.RemoveAllAsync(new PersistedGrantFilter { @@ -361,7 +365,7 @@ await store.RemoveAllAsync(new PersistedGrantFilter await PopulateDb(); await using (var context = new PersistedGrantDbContext(options, StoreOptions)) { - var store = new PersistedGrantStore(context, FakeLogger.Create()); + var store = new PersistedGrantStore(context, _telemetry, FakeLogger.Create()); await store.RemoveAllAsync(new PersistedGrantFilter { @@ -375,7 +379,7 @@ await store.RemoveAllAsync(new PersistedGrantFilter await PopulateDb(); await using (var context = new PersistedGrantDbContext(options, StoreOptions)) { - var store = new PersistedGrantStore(context, FakeLogger.Create()); + var store = new PersistedGrantStore(context, _telemetry, FakeLogger.Create()); await store.RemoveAllAsync(new PersistedGrantFilter { @@ -390,7 +394,7 @@ await store.RemoveAllAsync(new PersistedGrantFilter await PopulateDb(); await using (var context = new PersistedGrantDbContext(options, StoreOptions)) { - var store = new PersistedGrantStore(context, FakeLogger.Create()); + var store = new PersistedGrantStore(context, _telemetry, FakeLogger.Create()); await store.RemoveAllAsync(new PersistedGrantFilter { @@ -435,7 +439,7 @@ public async Task Store_should_create_new_record_if_key_does_not_exist(DbContext await using (var context = new PersistedGrantDbContext(options, StoreOptions)) { - var store = new PersistedGrantStore(context, FakeLogger.Create()); + var store = new PersistedGrantStore(context, _telemetry, FakeLogger.Create()); await store.StoreAsync(persistedGrant); } @@ -460,7 +464,7 @@ public async Task Store_should_update_record_if_key_already_exists(DbContextOpti var newDate = persistedGrant.Expiration.Value.AddHours(1); await using (var context = new PersistedGrantDbContext(options, StoreOptions)) { - var store = new PersistedGrantStore(context, FakeLogger.Create()); + var store = new PersistedGrantStore(context, _telemetry, FakeLogger.Create()); persistedGrant.Expiration = newDate; await store.StoreAsync(persistedGrant); } @@ -472,4 +476,46 @@ public async Task Store_should_update_record_if_key_already_exists(DbContextOpti Assert.Equal(newDate, persistedGrant.Expiration); } } + + [Theory, MemberData(nameof(TestDatabaseProviders))] + public async Task PublicMethods_WhenCalled_ShouldTelemetryTrace(DbContextOptions options) + { + List<(Func actMethod, string traceMethodName)> methods + = new() + { + (store => store.StoreAsync(CreateTestObject()), "StoreAsync"), + (store => store.GetAsync("test"), "GetAsync"), + (store => store.GetAllAsync(new PersistedGrantFilter(){ ClientId = "test" }), "GetAllAsync"), + (store => store.RemoveAsync("test" ), "RemoveAsync"), + (store => store.RemoveAllAsync(new PersistedGrantFilter(){ ClientId = "clientid" }), "RemoveAllAsync") + }; + + foreach (var method in methods) + { + var trace = Mock.Of(); + Mock.Get(_telemetry).Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(trace); + Mock.Get(trace).Setup(t => t.AddTag(It.IsAny(), It.IsAny())).Returns(trace); + Mock.Get(trace).Setup(t => t.AddTag(It.IsAny(), It.IsAny())).Returns(trace); + + using (var context = new PersistedGrantDbContext(options, StoreOptions)) + { + var store = new PersistedGrantStore(context, _telemetry, FakeLogger.Create()); + await method.actMethod(store); + + Mock.Get(_telemetry) + .Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Stores, store, method.traceMethodName), Times.Once); + Mock.Get(trace).Verify(t => t.Dispose(), Times.Once); + } + } + + // Assert all methods covered + typeof(PersistedGrantStore).GetMethods() + .Where(m => m.IsPublic && !m.IsStatic && !m.IsSpecialName) + .Where(m => m.DeclaringType == typeof(PersistedGrantStore)) + .Select(m => m.Name) + .Distinct() + .Should().BeEquivalentTo(methods.Select(m => m.traceMethodName)); + } } \ No newline at end of file diff --git a/src/EntityFramework.Storage/test/IntegrationTests/Stores/ResourceStoreTests.cs b/src/EntityFramework.Storage/test/IntegrationTests/Stores/ResourceStoreTests.cs index efed815a1..6eac4ae9e 100644 --- a/src/EntityFramework.Storage/test/IntegrationTests/Stores/ResourceStoreTests.cs +++ b/src/EntityFramework.Storage/test/IntegrationTests/Stores/ResourceStoreTests.cs @@ -7,12 +7,15 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using AwesomeAssertions; using Open.IdentityServer.EntityFramework.DbContexts; using Open.IdentityServer.EntityFramework.Options; using Open.IdentityServer.EntityFramework.Stores; using Open.IdentityServer.Models; using Microsoft.EntityFrameworkCore; +using Moq; using Open.IdentityServer.EntityFramework.Mappers; +using Open.IdentityServer.Services; using Open.IdentityServer.Utility; using Xunit; @@ -20,6 +23,8 @@ namespace Open.IdentityServer.EntityFramework.IntegrationTests.Stores; public class ScopeStoreTests : IntegrationTest { + private ITelemetryService _telemetry = Mock.Of(); + public ScopeStoreTests(DatabaseProviderFixture fixture) : base(fixture) { foreach (var row in TestDatabaseProviders) @@ -73,7 +78,6 @@ private static ApiScope CreateApiScopeTestResource() }; } - [Theory, MemberData(nameof(TestDatabaseProviders))] public async Task FindApiResourcesByNameAsync_WhenResourceExists_ExpectResourceAndCollectionsReturned(DbContextOptions options) { @@ -88,7 +92,7 @@ public async Task FindApiResourcesByNameAsync_WhenResourceExists_ExpectResourceA ApiResource foundResource; using (var context = new ConfigurationDbContext(options, StoreOptions)) { - var store = new ResourceStore(context, FakeLogger.Create()); + var store = new ResourceStore(context, _telemetry, FakeLogger.Create()); foundResource = (await store.FindApiResourcesByNameAsync(new[] { resource.Name })).SingleOrDefault(); } @@ -118,7 +122,7 @@ public async Task FindApiResourcesByNameAsync_WhenResourcesExist_ExpectOnlyResou ApiResource foundResource; using (var context = new ConfigurationDbContext(options, StoreOptions)) { - var store = new ResourceStore(context, FakeLogger.Create()); + var store = new ResourceStore(context, _telemetry, FakeLogger.Create()); foundResource = (await store.FindApiResourcesByNameAsync(new[] { resource.Name })).SingleOrDefault(); } @@ -133,9 +137,6 @@ public async Task FindApiResourcesByNameAsync_WhenResourcesExist_ExpectOnlyResou Assert.NotEmpty(foundResource.Scopes); } - - - [Theory, MemberData(nameof(TestDatabaseProviders))] public async Task FindApiResourcesByScopeNameAsync_WhenResourcesExist_ExpectResourcesReturned(DbContextOptions options) { @@ -153,7 +154,7 @@ public async Task FindApiResourcesByScopeNameAsync_WhenResourcesExist_ExpectReso IEnumerable resources; using (var context = new ConfigurationDbContext(options, StoreOptions)) { - var store = new ResourceStore(context, FakeLogger.Create()); + var store = new ResourceStore(context, _telemetry, FakeLogger.Create()); resources = await store.FindApiResourcesByScopeNameAsync(new List { testApiScope.Name @@ -187,7 +188,7 @@ public async Task FindApiResourcesByScopeNameAsync_WhenResourcesExist_ExpectOnly IEnumerable resources; using (var context = new ConfigurationDbContext(options, StoreOptions)) { - var store = new ResourceStore(context, FakeLogger.Create()); + var store = new ResourceStore(context, _telemetry, FakeLogger.Create()); resources = await store.FindApiResourcesByScopeNameAsync(new[] { testApiScope.Name }); } @@ -196,9 +197,6 @@ public async Task FindApiResourcesByScopeNameAsync_WhenResourcesExist_ExpectOnly Assert.NotNull(resources.Single(x => x.Name == testApiResource.Name)); } - - - [Theory, MemberData(nameof(TestDatabaseProviders))] public async Task FindIdentityResourcesByScopeNameAsync_WhenResourceExists_ExpectResourceAndCollectionsReturned(DbContextOptions options) { @@ -213,7 +211,7 @@ public async Task FindIdentityResourcesByScopeNameAsync_WhenResourceExists_Expec IList resources; using (var context = new ConfigurationDbContext(options, StoreOptions)) { - var store = new ResourceStore(context, FakeLogger.Create()); + var store = new ResourceStore(context, _telemetry, FakeLogger.Create()); resources = (await store.FindIdentityResourcesByScopeNameAsync(new List { resource.Name @@ -244,7 +242,7 @@ public async Task FindIdentityResourcesByScopeNameAsync_WhenResourcesExist_Expec IList resources; using (var context = new ConfigurationDbContext(options, StoreOptions)) { - var store = new ResourceStore(context, FakeLogger.Create()); + var store = new ResourceStore(context, _telemetry, FakeLogger.Create()); resources = (await store.FindIdentityResourcesByScopeNameAsync(new List { resource.Name @@ -272,7 +270,7 @@ public async Task FindApiScopesByNameAsync_WhenResourceExists_ExpectResourceAndC IList resources; using (var context = new ConfigurationDbContext(options, StoreOptions)) { - var store = new ResourceStore(context, FakeLogger.Create()); + var store = new ResourceStore(context, _telemetry, FakeLogger.Create()); resources = (await store.FindApiScopesByNameAsync(new List { resource.Name @@ -303,7 +301,7 @@ public async Task FindApiScopesByNameAsync_WhenResourcesExist_ExpectOnlyRequeste IList resources; using (var context = new ConfigurationDbContext(options, StoreOptions)) { - var store = new ResourceStore(context, FakeLogger.Create()); + var store = new ResourceStore(context, _telemetry, FakeLogger.Create()); resources = (await store.FindApiScopesByNameAsync(new List { resource.Name @@ -315,9 +313,6 @@ public async Task FindApiScopesByNameAsync_WhenResourcesExist_ExpectOnlyRequeste Assert.NotNull(resources.Single(x => x.Name == resource.Name)); } - - - [Theory, MemberData(nameof(TestDatabaseProviders))] public async Task GetAllResources_WhenAllResourcesRequested_ExpectAllResourcesIncludingHidden(DbContextOptions options) { @@ -353,7 +348,7 @@ public async Task GetAllResources_WhenAllResourcesRequested_ExpectAllResourcesIn Resources resources; using (var context = new ConfigurationDbContext(options, StoreOptions)) { - var store = new ResourceStore(context, FakeLogger.Create()); + var store = new ResourceStore(context, _telemetry, FakeLogger.Create()); resources = await store.GetAllResourcesAsync(); } @@ -371,4 +366,47 @@ public async Task GetAllResources_WhenAllResourcesRequested_ExpectAllResourcesIn Assert.Contains(resources.ApiScopes, x => x.Name == visibleApiScope.Name); Assert.Contains(resources.ApiScopes, x => x.Name == hiddenApiScope.Name); } + + [Theory, MemberData(nameof(TestDatabaseProviders))] + public async Task PublicMethods_WhenCalled_ShouldTelemetryTrace(DbContextOptions options) + { + List<(Func actMethod, string traceMethodName)> methods + = new() + { + (store => store.FindApiResourcesByNameAsync(new[] { "test" }), "FindApiResourcesByNameAsync"), + (store => store.FindApiResourcesByScopeNameAsync(new[] { "test" }), "FindApiResourcesByScopeNameAsync"), + (store => store.FindIdentityResourcesByScopeNameAsync(new[] { "test" }), "FindIdentityResourcesByScopeNameAsync"), + (store => store.FindApiScopesByNameAsync(new[] { "test" }), "FindApiScopesByNameAsync"), + (store => store.GetAllResourcesAsync(), "GetAllResourcesAsync") + }; + + foreach (var method in methods) + { + var trace = Mock.Of(); + Mock.Get(_telemetry).Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(trace); + Mock.Get(trace).Setup(t => t.AddTag(It.IsAny(), It.IsAny())).Returns(trace); + Mock.Get(trace).Setup(t => t.AddTag(It.IsAny(), It.IsAny())).Returns(trace); + + using (var context = new ConfigurationDbContext(options, StoreOptions)) + { + var store = new ResourceStore(context, _telemetry, FakeLogger.Create()); + await method.actMethod(store); + + Mock.Get(_telemetry) + .Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Stores, store, method.traceMethodName), Times.Once); + Mock.Get(trace) + .Verify(t => t.Dispose(), Times.Once); + } + } + + // Assert all methods covered + typeof(ResourceStore).GetMethods() + .Where(m => m.IsPublic && !m.IsStatic && !m.IsSpecialName) + .Where(m => m.DeclaringType == typeof(ResourceStore)) + .Select(m => m.Name) + .Distinct() + .Should().BeEquivalentTo(methods.Select(m => m.traceMethodName)); + } } \ No newline at end of file diff --git a/src/Open.IdentityServer/host/Host.csproj b/src/Open.IdentityServer/host/Host.csproj index 9b55fec48..1bed193a5 100644 --- a/src/Open.IdentityServer/host/Host.csproj +++ b/src/Open.IdentityServer/host/Host.csproj @@ -15,6 +15,10 @@ + + + + diff --git a/src/Open.IdentityServer/host/Program.cs b/src/Open.IdentityServer/host/Program.cs index d35d6835f..87f41d99e 100644 --- a/src/Open.IdentityServer/host/Program.cs +++ b/src/Open.IdentityServer/host/Program.cs @@ -9,6 +9,8 @@ using Serilog.Sinks.SystemConsole.Themes; using System; using System.Diagnostics; +using Microsoft.Extensions.Logging; +using OpenTelemetry.Logs; namespace IdentityServerHost; @@ -36,7 +38,7 @@ public static int Main(string[] args) // flushToDiskInterval: TimeSpan.FromSeconds(1)) .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}", theme: AnsiConsoleTheme.Code) .CreateLogger(); - + try { Log.Information("Starting host..."); @@ -57,6 +59,14 @@ public static int Main(string[] args) public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .UseSerilog() + .ConfigureLogging((ctx, logging) => + { + logging.AddOpenTelemetry(opts => + { + opts.IncludeFormattedMessage = true; + opts.AddConsoleExporter(); + }); + }) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); diff --git a/src/Open.IdentityServer/host/Startup.cs b/src/Open.IdentityServer/host/Startup.cs index d17c61c15..f0a238237 100644 --- a/src/Open.IdentityServer/host/Startup.cs +++ b/src/Open.IdentityServer/host/Startup.cs @@ -20,11 +20,16 @@ using Microsoft.AspNetCore.Authentication.Certificate; using Microsoft.AspNetCore.HttpOverrides; using IdentityServerHost.Quickstart.UI; +using Microsoft.AspNetCore.Hosting; using Open.IdentityServer.Utility; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; + namespace IdentityServerHost; -public class Startup +public class Startup { private readonly IConfiguration _config; @@ -78,7 +83,6 @@ public void ConfigureServices(IServiceCollection services) // options.ConfigureDbContext = b => b.UseSqlite(connectionString, // sql => sql.MigrationsAssembly(migrationsAssembly)); // }); - services.AddExternalIdentityProviders(); @@ -97,6 +101,31 @@ public void ConfigureServices(IServiceCollection services) return Task.FromResult(principal); }); + + services.AddOpenTelemetry() + .ConfigureResource(r => + { + r.AddService( + serviceName: "Demo Open.IdentityServer"); + }) + .WithMetrics(m => + { + m + .AddMeter(TelemetryConstants.MetricsConstants.MeterName) + .AddConsoleExporter(); + }) + .WithTracing(t => + { + t + .AddAspNetCoreInstrumentation() + .AddSource(TelemetryConstants.TraceCategories.Basic) + .AddSource(TelemetryConstants.TraceCategories.Cache) + .AddSource(TelemetryConstants.TraceCategories.Services) + .AddSource(TelemetryConstants.TraceCategories.Stores) + .AddSource(TelemetryConstants.TraceCategories.Validation) + .AddConsoleExporter() + ; + }); } public void Configure(IApplicationBuilder app) diff --git a/src/Open.IdentityServer/src/Configuration/DependencyInjection/BuilderExtensions/Additional.cs b/src/Open.IdentityServer/src/Configuration/DependencyInjection/BuilderExtensions/Additional.cs index 85f9562d3..892767703 100644 --- a/src/Open.IdentityServer/src/Configuration/DependencyInjection/BuilderExtensions/Additional.cs +++ b/src/Open.IdentityServer/src/Configuration/DependencyInjection/BuilderExtensions/Additional.cs @@ -383,8 +383,9 @@ public static IHttpClientBuilder AddBackChannelLogoutHttpClient(this IIdentitySe var httpClientFactory = s.GetRequiredService(); var httpClient = httpClientFactory.CreateClient(name); var loggerFactory = s.GetRequiredService(); + var telemetry = s.GetRequiredService(); - return new DefaultBackChannelLogoutHttpClient(httpClient, loggerFactory); + return new DefaultBackChannelLogoutHttpClient(httpClient, telemetry, loggerFactory); }); return httpBuilder; @@ -421,8 +422,9 @@ public static IHttpClientBuilder AddJwtRequestUriHttpClient(this IIdentityServer var httpClient = httpClientFactory.CreateClient(name); var loggerFactory = s.GetRequiredService(); var options = s.GetRequiredService(); + var telemetry = s.GetRequiredService(); - return new DefaultJwtRequestUriHttpClient(httpClient, options, loggerFactory); + return new DefaultJwtRequestUriHttpClient(httpClient, options, telemetry, loggerFactory); }); return httpBuilder; diff --git a/src/Open.IdentityServer/src/Configuration/DependencyInjection/BuilderExtensions/Core.cs b/src/Open.IdentityServer/src/Configuration/DependencyInjection/BuilderExtensions/Core.cs index 3bd54ef57..7cd2d4007 100644 --- a/src/Open.IdentityServer/src/Configuration/DependencyInjection/BuilderExtensions/Core.cs +++ b/src/Open.IdentityServer/src/Configuration/DependencyInjection/BuilderExtensions/Core.cs @@ -182,6 +182,8 @@ public static IIdentityServerBuilder AddPluggableServices(this IIdentityServerBu builder.Services.TryAddTransient(); builder.Services.TryAddTransient(); builder.Services.TryAddTransient(); + + builder.Services.TryAddSingleton(); builder.AddJwtRequestUriHttpClient(); builder.AddBackChannelLogoutHttpClient(); diff --git a/src/Open.IdentityServer/src/Constants.cs b/src/Open.IdentityServer/src/Constants.cs index a0aa8e7f4..487ef5540 100644 --- a/src/Open.IdentityServer/src/Constants.cs +++ b/src/Open.IdentityServer/src/Constants.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -245,6 +246,8 @@ public static class EnvironmentKeys [Obsolete("The IdentityServerOrigin constant is obsolete.")] public const string IdentityServerOrigin = "idsvr:IdentityServerOrigin"; // todo: deprecate public const string SignOutCalled = "idsvr:IdentityServerSignOutCalled"; + + internal const string OriginalRequestPath = "idsvr:OriginalRequestPath"; } public static class TokenTypeHints diff --git a/src/Open.IdentityServer/src/Endpoints/AuthorizeCallbackEndpoint.cs b/src/Open.IdentityServer/src/Endpoints/AuthorizeCallbackEndpoint.cs index a4c2c4c31..8555cd39a 100644 --- a/src/Open.IdentityServer/src/Endpoints/AuthorizeCallbackEndpoint.cs +++ b/src/Open.IdentityServer/src/Endpoints/AuthorizeCallbackEndpoint.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. using System.Collections.Specialized; @@ -21,6 +22,7 @@ namespace Open.IdentityServer.Endpoints; internal class AuthorizeCallbackEndpoint : AuthorizeEndpointBase { private readonly IConsentMessageStore _consentResponseStore; + private readonly ITelemetryService _telemetry; private readonly IAuthorizationParametersMessageStore _authorizationParametersMessageStore; public AuthorizeCallbackEndpoint( @@ -32,15 +34,19 @@ public AuthorizeCallbackEndpoint( IAuthorizeResponseGenerator authorizeResponseGenerator, IUserSession userSession, IConsentMessageStore consentResponseStore, + ITelemetryService telemetry, IAuthorizationParametersMessageStore authorizationParametersMessageStore = null) - : base(events, logger, options, validator, interactionGenerator, authorizeResponseGenerator, userSession) + : base(events, logger, options, validator, interactionGenerator, authorizeResponseGenerator, userSession, telemetry) { _consentResponseStore = consentResponseStore; + _telemetry = telemetry; _authorizationParametersMessageStore = authorizationParametersMessageStore; } public override async Task ProcessAsync(HttpContext context) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Basic, this); + if (!HttpMethods.IsGet(context.Request.Method)) { Logger.LogWarning("Invalid HTTP method for authorize endpoint."); diff --git a/src/Open.IdentityServer/src/Endpoints/AuthorizeEndpoint.cs b/src/Open.IdentityServer/src/Endpoints/AuthorizeEndpoint.cs index 50da9b402..e998d94e0 100644 --- a/src/Open.IdentityServer/src/Endpoints/AuthorizeEndpoint.cs +++ b/src/Open.IdentityServer/src/Endpoints/AuthorizeEndpoint.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. using System.Collections.Specialized; @@ -18,6 +19,8 @@ namespace Open.IdentityServer.Endpoints; internal class AuthorizeEndpoint : AuthorizeEndpointBase { + private readonly ITelemetryService _telemetry; + public AuthorizeEndpoint( IEventService events, ILogger logger, @@ -25,13 +28,17 @@ public AuthorizeEndpoint( IAuthorizeRequestValidator validator, IAuthorizeInteractionResponseGenerator interactionGenerator, IAuthorizeResponseGenerator authorizeResponseGenerator, - IUserSession userSession) - : base(events, logger, options, validator, interactionGenerator, authorizeResponseGenerator, userSession) + IUserSession userSession, + ITelemetryService telemetry) + : base(events, logger, options, validator, interactionGenerator, authorizeResponseGenerator, userSession, telemetry) { + _telemetry = telemetry; } public override async Task ProcessAsync(HttpContext context) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Basic, this); + Logger.LogDebug("Start authorize request"); NameValueCollection values; diff --git a/src/Open.IdentityServer/src/Endpoints/AuthorizeEndpointBase.cs b/src/Open.IdentityServer/src/Endpoints/AuthorizeEndpointBase.cs index 37b1338bc..49d3c07bc 100644 --- a/src/Open.IdentityServer/src/Endpoints/AuthorizeEndpointBase.cs +++ b/src/Open.IdentityServer/src/Endpoints/AuthorizeEndpointBase.cs @@ -1,7 +1,9 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. using System; +using System.Collections.Generic; using System.Collections.Specialized; using System.Security.Claims; using System.Threading.Tasks; @@ -23,6 +25,7 @@ namespace Open.IdentityServer.Endpoints; internal abstract class AuthorizeEndpointBase : IEndpointHandler { private readonly IAuthorizeResponseGenerator _authorizeResponseGenerator; + private readonly ITelemetryService _telemetry; private readonly IEventService _events; private readonly IdentityServerOptions _options; @@ -38,7 +41,8 @@ protected AuthorizeEndpointBase( IAuthorizeRequestValidator validator, IAuthorizeInteractionResponseGenerator interactionGenerator, IAuthorizeResponseGenerator authorizeResponseGenerator, - IUserSession userSession) + IUserSession userSession, + ITelemetryService telemetry) { _events = events; _options = options; @@ -46,6 +50,7 @@ protected AuthorizeEndpointBase( _validator = validator; _interactionGenerator = interactionGenerator; _authorizeResponseGenerator = authorizeResponseGenerator; + _telemetry = telemetry; UserSession = userSession; } @@ -171,6 +176,9 @@ private void LogTokens(AuthorizeResponse response) private Task RaiseFailureEventAsync(ValidatedAuthorizeRequest request, string error, string errorDescription) { + _telemetry.CountTokenIssued(request?.ClientId ?? "unknown-client", + request?.GrantType ?? "unknown-grant-type", + error); return _events.RaiseAsync(new TokenIssuedFailureEvent(request, error, errorDescription)); } @@ -179,9 +187,10 @@ private Task RaiseResponseEventAsync(AuthorizeResponse response) if (!response.IsError) { LogTokens(response); + _telemetry.CountTokenIssued(response.Request.ClientId, response.Request.GrantType); return _events.RaiseAsync(new TokenIssuedSuccessEvent(response)); } - + return RaiseFailureEventAsync(response.Request, response.Error, response.ErrorDescription); } } \ No newline at end of file diff --git a/src/Open.IdentityServer/src/Endpoints/CheckSessionEndpoint.cs b/src/Open.IdentityServer/src/Endpoints/CheckSessionEndpoint.cs index b7d68fb7d..6d8e8996a 100644 --- a/src/Open.IdentityServer/src/Endpoints/CheckSessionEndpoint.cs +++ b/src/Open.IdentityServer/src/Endpoints/CheckSessionEndpoint.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -8,20 +9,27 @@ using Microsoft.Extensions.Logging; using System.Net; using System.Threading.Tasks; +using Open.IdentityServer.Services; namespace Open.IdentityServer.Endpoints; internal class CheckSessionEndpoint : IEndpointHandler { + private readonly ITelemetryService _telemetry; private readonly ILogger _logger; - public CheckSessionEndpoint(ILogger logger) + public CheckSessionEndpoint( + ITelemetryService telemetryService, + ILogger logger) { + _telemetry = telemetryService; _logger = logger; } public Task ProcessAsync(HttpContext context) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Basic, this); + IEndpointResult result; if (!HttpMethods.IsGet(context.Request.Method)) diff --git a/src/Open.IdentityServer/src/Endpoints/DeviceAuthorizationEndpoint.cs b/src/Open.IdentityServer/src/Endpoints/DeviceAuthorizationEndpoint.cs index 06e330ddf..fe593684a 100644 --- a/src/Open.IdentityServer/src/Endpoints/DeviceAuthorizationEndpoint.cs +++ b/src/Open.IdentityServer/src/Endpoints/DeviceAuthorizationEndpoint.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -26,6 +27,7 @@ internal class DeviceAuthorizationEndpoint : IEndpointHandler private readonly IDeviceAuthorizationRequestValidator _requestValidator; private readonly IDeviceAuthorizationResponseGenerator _responseGenerator; private readonly IEventService _events; + private readonly ITelemetryService _telemetry; private readonly ILogger _logger; /// @@ -35,18 +37,21 @@ internal class DeviceAuthorizationEndpoint : IEndpointHandler /// The request validator. /// The response generator. /// The events service. + /// The telemetry service /// The logger. public DeviceAuthorizationEndpoint( IClientSecretValidator clientValidator, IDeviceAuthorizationRequestValidator requestValidator, IDeviceAuthorizationResponseGenerator responseGenerator, IEventService events, + ITelemetryService telemetry, ILogger logger) { _clientValidator = clientValidator; _requestValidator = requestValidator; _responseGenerator = responseGenerator; _events = events; + _telemetry = telemetry; _logger = logger; } @@ -57,6 +62,7 @@ public DeviceAuthorizationEndpoint( /// A task that resolves to an representing either a device authorization response or an error response. public async Task ProcessAsync(HttpContext context) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Basic, this); _logger.LogTrace("Processing device authorize request."); // validate HTTP @@ -76,13 +82,14 @@ private async Task ProcessDeviceAuthorizationRequestAsync(HttpC // validate client var clientResult = await _clientValidator.ValidateAsync(context); if (clientResult.Client == null) return Error(OidcConstants.TokenErrors.InvalidClient); - + // validate request var form = (await context.Request.ReadFormAsync()).AsNameValueCollection(); var requestResult = await _requestValidator.ValidateAsync(form, clientResult); if (requestResult.IsError) { + _telemetry.CountDeviceAuthentication(clientResult.Client.ClientId ?? "No client id found", requestResult.Error); await _events.RaiseAsync(new DeviceAuthorizationFailureEvent(requestResult)); return Error(requestResult.Error, requestResult.ErrorDescription); } @@ -93,6 +100,8 @@ private async Task ProcessDeviceAuthorizationRequestAsync(HttpC _logger.LogTrace("Calling into device authorize response generator: {type}", _responseGenerator.GetType().FullName); var response = await _responseGenerator.ProcessAsync(requestResult, baseUrl); + + _telemetry.CountDeviceAuthentication(clientResult.Client.ClientId); await _events.RaiseAsync(new DeviceAuthorizationSuccessEvent(response, requestResult)); // return result @@ -109,30 +118,8 @@ private TokenErrorResult Error(string error, string errorDescription = null, Dic Custom = custom }; - _logger.LogError("Device authorization error: {error}:{errorDescriptions}", error, error ?? "-no message-"); + _logger.LogError("Device authorization error: {error}:{errorDescriptions}", error, errorDescription ?? "-no message-"); return new TokenErrorResult(response); } - - private void LogResponse(DeviceAuthorizationResponse response, DeviceAuthorizationRequestValidationResult requestResult) - { - var clientId = $"{requestResult.ValidatedRequest.Client.ClientId} ({requestResult.ValidatedRequest.Client?.ClientName ?? "no name set"})"; - - if (response.DeviceCode != null) - { - _logger.LogTrace("Device code issued for {clientId}: {deviceCode}", clientId, response.DeviceCode); - } - if (response.UserCode != null) - { - _logger.LogTrace("User code issued for {clientId}: {userCode}", clientId, response.UserCode); - } - if (response.VerificationUri != null) - { - _logger.LogTrace("Verification URI issued for {clientId}: {verificationUri}", clientId, response.VerificationUri); - } - if (response.VerificationUriComplete != null) - { - _logger.LogTrace("Verification URI (Complete) issued for {clientId}: {verificationUriComplete}", clientId, response.VerificationUriComplete); - } - } } \ No newline at end of file diff --git a/src/Open.IdentityServer/src/Endpoints/DiscoveryEndpoint.cs b/src/Open.IdentityServer/src/Endpoints/DiscoveryEndpoint.cs index d1708dbed..02d0e9da4 100644 --- a/src/Open.IdentityServer/src/Endpoints/DiscoveryEndpoint.cs +++ b/src/Open.IdentityServer/src/Endpoints/DiscoveryEndpoint.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. using System.Net; @@ -10,6 +11,7 @@ using Open.IdentityServer.ResponseHandling; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; +using Open.IdentityServer.Services; namespace Open.IdentityServer.Endpoints; @@ -20,19 +22,23 @@ internal class DiscoveryEndpoint : IEndpointHandler private readonly IdentityServerOptions _options; private readonly IDiscoveryResponseGenerator _responseGenerator; + private readonly ITelemetryService _telemetry; public DiscoveryEndpoint( IdentityServerOptions options, IDiscoveryResponseGenerator responseGenerator, + ITelemetryService telemetry, ILogger logger) { _logger = logger; _options = options; _responseGenerator = responseGenerator; + _telemetry = telemetry; } public async Task ProcessAsync(HttpContext context) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Basic, this); _logger.LogTrace("Processing discovery request."); // validate HTTP diff --git a/src/Open.IdentityServer/src/Endpoints/DiscoveryKeyEndpoint.cs b/src/Open.IdentityServer/src/Endpoints/DiscoveryKeyEndpoint.cs index 28c0a543d..c0c95baf3 100644 --- a/src/Open.IdentityServer/src/Endpoints/DiscoveryKeyEndpoint.cs +++ b/src/Open.IdentityServer/src/Endpoints/DiscoveryKeyEndpoint.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. using System.Net; @@ -9,6 +10,7 @@ using Open.IdentityServer.ResponseHandling; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; +using Open.IdentityServer.Services; namespace Open.IdentityServer.Endpoints; @@ -19,19 +21,23 @@ internal class DiscoveryKeyEndpoint : IEndpointHandler private readonly IdentityServerOptions _options; private readonly IDiscoveryResponseGenerator _responseGenerator; + private readonly ITelemetryService _telemetry; public DiscoveryKeyEndpoint( IdentityServerOptions options, IDiscoveryResponseGenerator responseGenerator, + ITelemetryService telemetry, ILogger logger) { _logger = logger; _options = options; _responseGenerator = responseGenerator; + _telemetry = telemetry; } public async Task ProcessAsync(HttpContext context) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Basic, this); _logger.LogTrace("Processing discovery request."); // validate HTTP diff --git a/src/Open.IdentityServer/src/Endpoints/EndSessionCallbackEndpoint.cs b/src/Open.IdentityServer/src/Endpoints/EndSessionCallbackEndpoint.cs index f0a6440b5..ebf8b937e 100644 --- a/src/Open.IdentityServer/src/Endpoints/EndSessionCallbackEndpoint.cs +++ b/src/Open.IdentityServer/src/Endpoints/EndSessionCallbackEndpoint.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. using System.Net; @@ -9,24 +10,30 @@ using Open.IdentityServer.Validation; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; +using Open.IdentityServer.Services; namespace Open.IdentityServer.Endpoints; internal class EndSessionCallbackEndpoint : IEndpointHandler { private readonly IEndSessionRequestValidator _endSessionRequestValidator; + private readonly ITelemetryService _telemetry; private readonly ILogger _logger; public EndSessionCallbackEndpoint( IEndSessionRequestValidator endSessionRequestValidator, + ITelemetryService telemetry, ILogger logger) { _endSessionRequestValidator = endSessionRequestValidator; + _telemetry = telemetry; _logger = logger; } public async Task ProcessAsync(HttpContext context) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Basic, this); + if (!HttpMethods.IsGet(context.Request.Method)) { _logger.LogWarning("Invalid HTTP method for end session callback endpoint."); diff --git a/src/Open.IdentityServer/src/Endpoints/EndSessionEndpoint.cs b/src/Open.IdentityServer/src/Endpoints/EndSessionEndpoint.cs index 57d5d9e9f..fce17c574 100644 --- a/src/Open.IdentityServer/src/Endpoints/EndSessionEndpoint.cs +++ b/src/Open.IdentityServer/src/Endpoints/EndSessionEndpoint.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. using System.Collections.Specialized; @@ -21,19 +22,24 @@ internal class EndSessionEndpoint : IEndpointHandler private readonly ILogger _logger; private readonly IUserSession _userSession; + private readonly ITelemetryService _telemetry; public EndSessionEndpoint( IEndSessionRequestValidator endSessionRequestValidator, IUserSession userSession, + ITelemetryService telemetry, ILogger logger) { _endSessionRequestValidator = endSessionRequestValidator; _userSession = userSession; + _telemetry = telemetry; _logger = logger; } public async Task ProcessAsync(HttpContext context) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Basic, this); + NameValueCollection parameters; if (HttpMethods.IsGet(context.Request.Method)) { diff --git a/src/Open.IdentityServer/src/Endpoints/IntrospectionEndpoint.cs b/src/Open.IdentityServer/src/Endpoints/IntrospectionEndpoint.cs index a61bcfc75..a1370c064 100644 --- a/src/Open.IdentityServer/src/Endpoints/IntrospectionEndpoint.cs +++ b/src/Open.IdentityServer/src/Endpoints/IntrospectionEndpoint.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -23,6 +24,7 @@ namespace Open.IdentityServer.Endpoints; internal class IntrospectionEndpoint : IEndpointHandler { private readonly IIntrospectionResponseGenerator _responseGenerator; + private readonly ITelemetryService _telemetry; private readonly IEventService _events; private readonly ILogger _logger; private readonly IIntrospectionRequestValidator _requestValidator; @@ -34,18 +36,21 @@ internal class IntrospectionEndpoint : IEndpointHandler /// The API secret validator. /// The request validator. /// The generator. + /// The telemetry service /// The events. /// The logger. public IntrospectionEndpoint( IApiSecretValidator apiSecretValidator, IIntrospectionRequestValidator requestValidator, IIntrospectionResponseGenerator responseGenerator, + ITelemetryService telemetry, IEventService events, ILogger logger) { _apiSecretValidator = apiSecretValidator; _requestValidator = requestValidator; _responseGenerator = responseGenerator; + _telemetry = telemetry; _events = events; _logger = logger; } @@ -57,6 +62,7 @@ public IntrospectionEndpoint( /// A task that resolves to an representing either an introspection response or an error response. public async Task ProcessAsync(HttpContext context) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Basic, this); _logger.LogTrace("Processing introspection request."); // validate HTTP @@ -101,6 +107,7 @@ private async Task ProcessIntrospectionRequestAsync(HttpContext var validationResult = await _requestValidator.ValidateAsync(body.AsNameValueCollection(), apiResult.Resource); if (validationResult.IsError) { + _telemetry.CountTokenIntrospection(apiResult.Resource.Name, error: validationResult.Error); LogFailure(validationResult.Error, apiResult.Resource.Name); await _events.RaiseAsync(new TokenIntrospectionFailureEvent(apiResult.Resource.Name, validationResult.Error)); diff --git a/src/Open.IdentityServer/src/Endpoints/TokenEndpoint.cs b/src/Open.IdentityServer/src/Endpoints/TokenEndpoint.cs index c2ad5c145..a98483d45 100644 --- a/src/Open.IdentityServer/src/Endpoints/TokenEndpoint.cs +++ b/src/Open.IdentityServer/src/Endpoints/TokenEndpoint.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -26,6 +27,7 @@ internal class TokenEndpoint : IEndpointHandler private readonly ITokenRequestValidator _requestValidator; private readonly ITokenResponseGenerator _responseGenerator; private readonly IEventService _events; + private readonly ITelemetryService _telemetry; private readonly ILogger _logger; /// @@ -35,12 +37,13 @@ internal class TokenEndpoint : IEndpointHandler /// The request validator. /// The response generator. /// The events. + /// The telemetry service /// The logger. - public TokenEndpoint( - IClientSecretValidator clientValidator, - ITokenRequestValidator requestValidator, - ITokenResponseGenerator responseGenerator, - IEventService events, + public TokenEndpoint(IClientSecretValidator clientValidator, + ITokenRequestValidator requestValidator, + ITokenResponseGenerator responseGenerator, + IEventService events, + ITelemetryService telemetry, ILogger logger) { _clientValidator = clientValidator; @@ -48,6 +51,7 @@ public TokenEndpoint( _responseGenerator = responseGenerator; _events = events; _logger = logger; + _telemetry = telemetry; } /// @@ -57,6 +61,7 @@ public TokenEndpoint( /// A task that resolves to an representing either a token response or an error response. public async Task ProcessAsync(HttpContext context) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Basic, this); _logger.LogTrace("Processing token request."); // validate HTTP @@ -88,6 +93,7 @@ private async Task ProcessTokenRequestAsync(HttpContext context if (requestResult.IsError) { + _telemetry.CountTokenIssued(clientResult.Client.ClientId, requestResult.ValidatedRequest.GrantType, requestResult.Error); await _events.RaiseAsync(new TokenIssuedFailureEvent(requestResult)); return Error(requestResult.Error, requestResult.ErrorDescription, requestResult.CustomResponse); } @@ -96,6 +102,7 @@ private async Task ProcessTokenRequestAsync(HttpContext context _logger.LogTrace("Calling into token request response generator: {type}", _responseGenerator.GetType().FullName); var response = await _responseGenerator.ProcessAsync(requestResult); + _telemetry.CountTokenIssued(clientResult.Client.ClientId, requestResult.ValidatedRequest.GrantType); await _events.RaiseAsync(new TokenIssuedSuccessEvent(response, requestResult)); LogTokens(response, requestResult); diff --git a/src/Open.IdentityServer/src/Endpoints/TokenRevocationEndpoint.cs b/src/Open.IdentityServer/src/Endpoints/TokenRevocationEndpoint.cs index 43e7cdf24..a8ff5d9ca 100644 --- a/src/Open.IdentityServer/src/Endpoints/TokenRevocationEndpoint.cs +++ b/src/Open.IdentityServer/src/Endpoints/TokenRevocationEndpoint.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -27,6 +28,7 @@ internal class TokenRevocationEndpoint : IEndpointHandler private readonly ITokenRevocationRequestValidator _requestValidator; private readonly ITokenRevocationResponseGenerator _responseGenerator; private readonly IEventService _events; + private readonly ITelemetryService _telemetry; /// /// Initializes a new instance of the class. @@ -36,11 +38,13 @@ internal class TokenRevocationEndpoint : IEndpointHandler /// The request validator. /// The response generator. /// The events. + /// The telemetry service public TokenRevocationEndpoint(ILogger logger, IClientSecretValidator clientValidator, ITokenRevocationRequestValidator requestValidator, ITokenRevocationResponseGenerator responseGenerator, - IEventService events) + IEventService events, + ITelemetryService telemetry) { _logger = logger; _clientValidator = clientValidator; @@ -48,6 +52,7 @@ public TokenRevocationEndpoint(ILogger logger, _responseGenerator = responseGenerator; _events = events; + _telemetry = telemetry; } /// @@ -57,6 +62,7 @@ public TokenRevocationEndpoint(ILogger logger, /// A task that resolves to an representing either a successful revocation response or an error response. public async Task ProcessAsync(HttpContext context) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Basic, this); _logger.LogTrace("Processing revocation request."); if (!HttpMethods.IsPost(context.Request.Method)) @@ -85,6 +91,7 @@ private async Task ProcessRevocationRequestAsync(HttpContext co if (clientValidationResult.IsError) { + _telemetry.CountTokenRevocation(clientValidationResult.Client?.ClientId ?? "unknown client", clientValidationResult.Error); return new TokenRevocationErrorResult(OidcConstants.TokenErrors.InvalidClient); } @@ -98,6 +105,7 @@ private async Task ProcessRevocationRequestAsync(HttpContext co if (requestValidationResult.IsError) { + _telemetry.CountTokenRevocation(requestValidationResult.Client?.ClientId ?? "unknown client", requestValidationResult.Error); return new TokenRevocationErrorResult(requestValidationResult.Error); } @@ -107,6 +115,7 @@ private async Task ProcessRevocationRequestAsync(HttpContext co if (response.Success) { _logger.LogInformation("Token revocation complete"); + _telemetry.CountTokenRevocation(requestValidationResult.Client.ClientId); await _events.RaiseAsync(new TokenRevokedSuccessEvent(requestValidationResult, requestValidationResult.Client)); } else diff --git a/src/Open.IdentityServer/src/Endpoints/UserInfoEndpoint.cs b/src/Open.IdentityServer/src/Endpoints/UserInfoEndpoint.cs index 2c83cbd61..6d6771d7e 100644 --- a/src/Open.IdentityServer/src/Endpoints/UserInfoEndpoint.cs +++ b/src/Open.IdentityServer/src/Endpoints/UserInfoEndpoint.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -10,6 +11,7 @@ using Open.IdentityServer.Endpoints.Results; using Microsoft.AspNetCore.Http; using System.Net; +using Open.IdentityServer.Services; namespace Open.IdentityServer.Endpoints; @@ -22,6 +24,7 @@ internal class UserInfoEndpoint : IEndpointHandler private readonly BearerTokenUsageValidator _tokenUsageValidator; private readonly IUserInfoRequestValidator _requestValidator; private readonly IUserInfoResponseGenerator _responseGenerator; + private readonly ITelemetryService _telemetry; private readonly ILogger _logger; /// @@ -30,16 +33,19 @@ internal class UserInfoEndpoint : IEndpointHandler /// The token usage validator. /// The request validator. /// The response generator. + /// The telemetry service /// The logger. public UserInfoEndpoint( BearerTokenUsageValidator tokenUsageValidator, IUserInfoRequestValidator requestValidator, IUserInfoResponseGenerator responseGenerator, + ITelemetryService telemetry, ILogger logger) { _tokenUsageValidator = tokenUsageValidator; _requestValidator = requestValidator; _responseGenerator = responseGenerator; + _telemetry = telemetry; _logger = logger; } @@ -50,6 +56,8 @@ public UserInfoEndpoint( /// A task that resolves to an representing either a userinfo response or an error response. public async Task ProcessAsync(HttpContext context) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Basic, this); + if (!HttpMethods.IsGet(context.Request.Method) && !HttpMethods.IsPost(context.Request.Method)) { _logger.LogWarning("Invalid HTTP method for userinfo endpoint."); diff --git a/src/Open.IdentityServer/src/Extensions/HttpContextExtensions.cs b/src/Open.IdentityServer/src/Extensions/HttpContextExtensions.cs index cfb3a38c4..f29f93b45 100644 --- a/src/Open.IdentityServer/src/Extensions/HttpContextExtensions.cs +++ b/src/Open.IdentityServer/src/Extensions/HttpContextExtensions.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -236,4 +237,15 @@ internal static async Task GetIdentityServerSignoutFrameCallbackUrlAsync // no sessions, so nothing to cleanup return null; } + + internal static string GetOriginalRequestPath(this HttpContext context) + { + PathString path = context.Request.Path; + if (context.Items.TryGetValue(Constants.EnvironmentKeys.OriginalRequestPath, out var originalPath)) + { + path = originalPath as PathString? ?? path; + } + + return path.ToString(); + } } \ No newline at end of file diff --git a/src/Open.IdentityServer/src/Hosting/IdentityServerMiddleware.cs b/src/Open.IdentityServer/src/Hosting/IdentityServerMiddleware.cs index 1d6b0edec..31a180a58 100644 --- a/src/Open.IdentityServer/src/Hosting/IdentityServerMiddleware.cs +++ b/src/Open.IdentityServer/src/Hosting/IdentityServerMiddleware.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -39,8 +40,15 @@ public IdentityServerMiddleware(RequestDelegate next, ILoggerThe user session. /// The event service. /// The service used to send back-channel logout notifications to clients when the user signs out. - /// /// A task that completes when the request has been handled by an IdentityServer endpoint or passed to the next middleware in the pipeline. - public async Task Invoke(HttpContext context, IEndpointRouter router, IUserSession session, IEventService events, IBackChannelLogoutService backChannelLogoutService) + /// The telemetry service + /// A task that completes when the request has been handled by an IdentityServer endpoint or passed to the next middleware in the pipeline. + public async Task Invoke( + HttpContext context, + IEndpointRouter router, + IUserSession session, + IEventService events, + IBackChannelLogoutService backChannelLogoutService, + ITelemetryService telemetryService) { // this will check the authentication session and from it emit the check session // cookie needed from JS-based signout clients. @@ -64,15 +72,21 @@ public async Task Invoke(HttpContext context, IEndpointRouter router, IUserSessi } }); + ITrace trace = null; try { var endpoint = router.Find(context); if (endpoint != null) { - _logger.LogInformation("Invoking IdentityServer endpoint: {endpointType} for {url}", endpoint.GetType().FullName, context.Request.Path.ToString()); + trace = telemetryService + .Trace(TelemetryConstants.TraceCategories.Basic, "IdentityServerProtocolRequest"); + using var activeRequest = telemetryService.BeginActiveRequest( + endpoint.GetType().FullName, context.GetOriginalRequestPath()); - var result = await endpoint.ProcessAsync(context); + _logger.LogInformation("Invoking IdentityServer endpoint: {endpointType} for {url}", + endpoint.GetType().FullName, context.Request.Path.ToString()); + var result = await endpoint.ProcessAsync(context); if (result != null) { _logger.LogTrace("Invoking result: {type}", result.GetType().FullName); @@ -84,10 +98,16 @@ public async Task Invoke(HttpContext context, IEndpointRouter router, IUserSessi } catch (Exception ex) { + trace?.AddException(ex); + telemetryService.CountInternalError(error: ex.GetType().FullName); await events.RaiseAsync(new UnhandledExceptionEvent(ex)); _logger.LogCritical(ex, "Unhandled exception: {exception}", ex.Message); throw; } + finally + { + trace?.Dispose(); + } await _next(context); } diff --git a/src/Open.IdentityServer/src/Hosting/MutualTlsEndpointMiddleware.cs b/src/Open.IdentityServer/src/Hosting/MutualTlsEndpointMiddleware.cs index d3fb0c351..69a921114 100644 --- a/src/Open.IdentityServer/src/Hosting/MutualTlsEndpointMiddleware.cs +++ b/src/Open.IdentityServer/src/Hosting/MutualTlsEndpointMiddleware.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. using System; @@ -86,8 +87,11 @@ public async Task Invoke(HttpContext context, IAuthenticationSchemeProvider sche subPath.ToString().EnsureLeadingSlash(); path = path.EnsureLeadingSlash(); + var oldPath = context.Request.Path; + context.Items[Constants.EnvironmentKeys.OriginalRequestPath] = oldPath; + _logger.LogDebug("Rewriting MTLS request from: {oldPath} to: {newPath}", - context.Request.Path.ToString(), path); + oldPath, path); context.Request.Path = path; } else diff --git a/src/Open.IdentityServer/src/ResponseHandling/Default/AuthorizeInteractionResponseGenerator.cs b/src/Open.IdentityServer/src/ResponseHandling/Default/AuthorizeInteractionResponseGenerator.cs index d6b691e8e..6d79f657c 100644 --- a/src/Open.IdentityServer/src/ResponseHandling/Default/AuthorizeInteractionResponseGenerator.cs +++ b/src/Open.IdentityServer/src/ResponseHandling/Default/AuthorizeInteractionResponseGenerator.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -38,6 +39,11 @@ public class AuthorizeInteractionResponseGenerator : IAuthorizeInteractionRespon /// The clock /// protected readonly TimeProvider Clock; + + /// + /// The telemetry + /// + protected readonly ITelemetryService Telemetry; /// /// Initializes a new instance of the class. @@ -46,16 +52,19 @@ public class AuthorizeInteractionResponseGenerator : IAuthorizeInteractionRespon /// The logger. /// The consent. /// The profile. + /// The telemetry. public AuthorizeInteractionResponseGenerator( TimeProvider clock, ILogger logger, IConsentService consent, - IProfileService profile) + IProfileService profile, + ITelemetryService telemetry) { Clock = clock; Logger = logger; Consent = consent; Profile = profile; + Telemetry = telemetry; } /// @@ -66,6 +75,7 @@ public AuthorizeInteractionResponseGenerator( /// A task that resolves to an indicating whether the user must log in, consent, or can proceed. public virtual async Task ProcessInteractionAsync(ValidatedAuthorizeRequest request, ConsentResponse consent = null) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Basic, this); Logger.LogTrace("ProcessInteractionAsync"); if (consent != null && diff --git a/src/Open.IdentityServer/src/ResponseHandling/Default/AuthorizeResponseGenerator.cs b/src/Open.IdentityServer/src/ResponseHandling/Default/AuthorizeResponseGenerator.cs index 77ce80d4f..493eb06fe 100644 --- a/src/Open.IdentityServer/src/ResponseHandling/Default/AuthorizeResponseGenerator.cs +++ b/src/Open.IdentityServer/src/ResponseHandling/Default/AuthorizeResponseGenerator.cs @@ -51,6 +51,11 @@ public class AuthorizeResponseGenerator : IAuthorizeResponseGenerator /// The key material service /// protected readonly IKeyMaterialService KeyMaterialService; + + /// + /// The telemetry service + /// + protected readonly ITelemetryService TelemetryService; /// /// Initializes a new instance of the class. @@ -61,13 +66,15 @@ public class AuthorizeResponseGenerator : IAuthorizeResponseGenerator /// The key material service used to retrieve signing credentials. /// The authorization code store. /// The events. + /// The telemetry service public AuthorizeResponseGenerator( TimeProvider clock, ITokenService tokenService, IKeyMaterialService keyMaterialService, IAuthorizationCodeStore authorizationCodeStore, ILogger logger, - IEventService events) + IEventService events, + ITelemetryService telemetry) { Clock = clock; TokenService = tokenService; @@ -75,6 +82,7 @@ public AuthorizeResponseGenerator( AuthorizationCodeStore = authorizationCodeStore; Events = events; Logger = logger; + TelemetryService = telemetry; } /// @@ -85,6 +93,11 @@ public AuthorizeResponseGenerator( /// invalid grant type: " + request.GrantType public virtual async Task CreateResponseAsync(ValidatedAuthorizeRequest request) { + using var trace = TelemetryService.Trace(TelemetryConstants.TraceCategories.Basic, this); + trace + ?.AddTag(TelemetryConstants.TagConstants.Client, request.Client.ClientId) + ?.AddTag(TelemetryConstants.TagConstants.GrantType, request.GrantType); + if (request.GrantType == GrantType.AuthorizationCode) { return await CreateCodeFlowResponseAsync(request); diff --git a/src/Open.IdentityServer/src/ResponseHandling/Default/DeviceAuthorizationResponseGenerator.cs b/src/Open.IdentityServer/src/ResponseHandling/Default/DeviceAuthorizationResponseGenerator.cs index b4ff4cf57..c94f6bd17 100644 --- a/src/Open.IdentityServer/src/ResponseHandling/Default/DeviceAuthorizationResponseGenerator.cs +++ b/src/Open.IdentityServer/src/ResponseHandling/Default/DeviceAuthorizationResponseGenerator.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -44,6 +45,11 @@ public class DeviceAuthorizationResponseGenerator : IDeviceAuthorizationResponse /// protected readonly ILogger Logger; + /// + /// The telemetry + /// + protected readonly ITelemetryService Telemetry; + /// /// Initializes a new instance of the class. /// @@ -51,14 +57,16 @@ public class DeviceAuthorizationResponseGenerator : IDeviceAuthorizationResponse /// The user code service. /// The device flow code service. /// The clock. + /// The telemetry /// The logger. - public DeviceAuthorizationResponseGenerator(IdentityServerOptions options, IUserCodeService userCodeService, IDeviceFlowCodeService deviceFlowCodeService, TimeProvider clock, ILogger logger) + public DeviceAuthorizationResponseGenerator(IdentityServerOptions options, IUserCodeService userCodeService, IDeviceFlowCodeService deviceFlowCodeService, TimeProvider clock, ITelemetryService telemetry, ILogger logger) { Options = options; UserCodeService = userCodeService; DeviceFlowCodeService = deviceFlowCodeService; Clock = clock; Logger = logger; + Telemetry = telemetry; } /// @@ -75,6 +83,9 @@ public virtual async Task ProcessAsync(DeviceAuthor if (validationResult.ValidatedRequest.Client == null) throw new ArgumentNullException(nameof(validationResult.ValidatedRequest.Client)); if (string.IsNullOrWhiteSpace(baseUrl)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(baseUrl)); + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Basic, this); + trace?.AddTag(TelemetryConstants.TagConstants.Client, validationResult.ValidatedRequest.ClientId); + Logger.LogTrace("Creating response for device authorization request"); var response = new DeviceAuthorizationResponse(); diff --git a/src/Open.IdentityServer/src/ResponseHandling/Default/DiscoveryResponseGenerator.cs b/src/Open.IdentityServer/src/ResponseHandling/Default/DiscoveryResponseGenerator.cs index 093017953..7439cbfbc 100644 --- a/src/Open.IdentityServer/src/ResponseHandling/Default/DiscoveryResponseGenerator.cs +++ b/src/Open.IdentityServer/src/ResponseHandling/Default/DiscoveryResponseGenerator.cs @@ -55,6 +55,11 @@ public class DiscoveryResponseGenerator : IDiscoveryResponseGenerator /// protected readonly ISecretsListParser SecretParsers; + /// + /// The telemetry + /// + protected readonly ITelemetryService Telemetry; + /// /// The logger /// @@ -69,6 +74,7 @@ public class DiscoveryResponseGenerator : IDiscoveryResponseGenerator /// The extension grants. /// The secret parsers. /// The resource owner validator. + /// The telemetry service /// The logger. public DiscoveryResponseGenerator( IdentityServerOptions options, @@ -77,6 +83,7 @@ public DiscoveryResponseGenerator( ExtensionGrantValidator extensionGrants, ISecretsListParser secretParsers, IResourceOwnerPasswordValidator resourceOwnerValidator, + ITelemetryService telemetry, ILogger logger) { Options = options; @@ -85,6 +92,7 @@ public DiscoveryResponseGenerator( ExtensionGrants = extensionGrants; SecretParsers = secretParsers; ResourceOwnerValidator = resourceOwnerValidator; + Telemetry = telemetry; Logger = logger; } @@ -95,6 +103,8 @@ public DiscoveryResponseGenerator( /// The issuer URI. public virtual async Task> CreateDiscoveryDocumentAsync(string baseUrl, string issuerUri) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Basic, this); + var entries = new Dictionary { { OidcConstants.Discovery.Issuer, issuerUri }, @@ -368,6 +378,8 @@ where scope.ShowInDiscoveryDocument /// public virtual async Task> CreateJwkDocumentAsync() { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Basic, this); + var webKeys = new List(); foreach (var key in await Keys.GetValidationKeysAsync()) diff --git a/src/Open.IdentityServer/src/ResponseHandling/Default/IntrospectionResponseGenerator.cs b/src/Open.IdentityServer/src/ResponseHandling/Default/IntrospectionResponseGenerator.cs index 23708003b..3f2fae3fa 100644 --- a/src/Open.IdentityServer/src/ResponseHandling/Default/IntrospectionResponseGenerator.cs +++ b/src/Open.IdentityServer/src/ResponseHandling/Default/IntrospectionResponseGenerator.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -27,6 +28,11 @@ public class IntrospectionResponseGenerator : IIntrospectionResponseGenerator /// protected readonly IEventService Events; + /// + /// The Telemetry service + /// + protected readonly ITelemetryService Telemetry; + /// /// The logger /// @@ -36,10 +42,12 @@ public class IntrospectionResponseGenerator : IIntrospectionResponseGenerator /// Initializes a new instance of the class. /// /// The events. + /// The Telemetry /// The logger. - public IntrospectionResponseGenerator(IEventService events, ILogger logger) + public IntrospectionResponseGenerator(IEventService events, ITelemetryService telemetry, ILogger logger) { Events = events; + Telemetry = telemetry; Logger = logger; } @@ -50,6 +58,9 @@ public IntrospectionResponseGenerator(IEventService events, ILoggerA task that resolves to a dictionary containing the introspection response claims, with active set to and scopes filtered to those the calling API is permitted to see, or a minimal inactive response if the token is invalid or the API has no matching scopes. public virtual async Task> ProcessAsync(IntrospectionRequestValidationResult validationResult) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Basic, this); + trace?.AddTag(TelemetryConstants.TagConstants.Api, validationResult.Api.Name); + Logger.LogTrace("Creating introspection response"); // standard response @@ -61,6 +72,7 @@ public virtual async Task> ProcessAsync(Introspection // token is invalid if (validationResult.IsActive == false) { + Telemetry.CountTokenIntrospection(validationResult.Api.Name, validationResult.IsActive); Logger.LogDebug("Creating introspection response for inactive token."); await Events.RaiseAsync(new TokenIntrospectionSuccessEvent(validationResult)); @@ -87,6 +99,7 @@ public virtual async Task> ProcessAsync(Introspection scopes = scopes.Where(x => allowedScopes.Contains(x)); response.Add("scope", scopes.ToSpaceSeparatedString()); + Telemetry.CountTokenIntrospection(validationResult.Api.Name, validationResult.IsActive); await Events.RaiseAsync(new TokenIntrospectionSuccessEvent(validationResult)); return response; } @@ -113,6 +126,7 @@ protected virtual async Task AreExpectedScopesPresentAsync(IntrospectionRe else { // no scopes for this API are found in the token + Telemetry.CountTokenIntrospection( validationResult.Api.Name, error: "Expected scopes are missing"); Logger.LogError("Expected scope {scopes} is missing in token", apiScopes); await Events.RaiseAsync(new TokenIntrospectionFailureEvent(validationResult.Api.Name, "Expected scopes are missing", validationResult.Token, apiScopes, tokenScopes.Select(s => s.Value))); } diff --git a/src/Open.IdentityServer/src/ResponseHandling/Default/TokenResponseGenerator.cs b/src/Open.IdentityServer/src/ResponseHandling/Default/TokenResponseGenerator.cs index 58ff42168..6e3c92ec7 100644 --- a/src/Open.IdentityServer/src/ResponseHandling/Default/TokenResponseGenerator.cs +++ b/src/Open.IdentityServer/src/ResponseHandling/Default/TokenResponseGenerator.cs @@ -52,9 +52,14 @@ public class TokenResponseGenerator : ITokenResponseGenerator protected readonly IClientStore Clients; /// - /// The clock + /// The clock /// protected readonly TimeProvider Clock; + + /// + /// The telemetry + /// + protected readonly ITelemetryService Telemetry; /// /// Initializes a new instance of the class. @@ -65,10 +70,11 @@ public class TokenResponseGenerator : ITokenResponseGenerator /// The scope parser. /// The resources. /// The clients. + /// The telemetry. /// The logger. public TokenResponseGenerator(TimeProvider clock, ITokenService tokenService, IRefreshTokenService refreshTokenService, IScopeParser scopeParser, IResourceStore resources, - IClientStore clients, ILogger logger) + IClientStore clients, ITelemetryService telemetry, ILogger logger) { Clock = clock; TokenService = tokenService; @@ -76,6 +82,7 @@ public TokenResponseGenerator(TimeProvider clock, ITokenService tokenService, ScopeParser = scopeParser; Resources = resources; Clients = clients; + Telemetry = telemetry; Logger = logger; } @@ -86,6 +93,9 @@ public TokenResponseGenerator(TimeProvider clock, ITokenService tokenService, /// A task that resolves to a for the requested grant type. public virtual async Task ProcessAsync(TokenRequestValidationResult request) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Basic, this); + trace?.AddTag(TelemetryConstants.TagConstants.GrantType, request.ValidatedRequest.GrantType); + switch (request.ValidatedRequest.GrantType) { case OidcConstants.GrantTypes.ClientCredentials: diff --git a/src/Open.IdentityServer/src/ResponseHandling/Default/TokenRevocationResponseGenerator.cs b/src/Open.IdentityServer/src/ResponseHandling/Default/TokenRevocationResponseGenerator.cs index 91ea8cea5..996a05fe6 100644 --- a/src/Open.IdentityServer/src/ResponseHandling/Default/TokenRevocationResponseGenerator.cs +++ b/src/Open.IdentityServer/src/ResponseHandling/Default/TokenRevocationResponseGenerator.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -6,6 +7,7 @@ using Open.IdentityServer.Validation; using Open.IdentityServer.Stores; using Microsoft.Extensions.Logging; +using Open.IdentityServer.Services; namespace Open.IdentityServer.ResponseHandling; @@ -31,6 +33,11 @@ public class TokenRevocationResponseGenerator : ITokenRevocationResponseGenerato /// protected readonly IRefreshTokenStore RefreshTokenStore; + /// + /// The telemetry service. + /// + protected readonly ITelemetryService Telemetry; + /// /// Gets the logger. /// @@ -44,11 +51,17 @@ public class TokenRevocationResponseGenerator : ITokenRevocationResponseGenerato /// /// The reference token store. /// The refresh token store. + /// The telemetry. /// The logger. - public TokenRevocationResponseGenerator(IReferenceTokenStore referenceTokenStore, IRefreshTokenStore refreshTokenStore, ILogger logger) + public TokenRevocationResponseGenerator( + IReferenceTokenStore referenceTokenStore, + IRefreshTokenStore refreshTokenStore, + ITelemetryService telemetry, + ILogger logger) { ReferenceTokenStore = referenceTokenStore; RefreshTokenStore = refreshTokenStore; + Telemetry = telemetry; Logger = logger; } @@ -59,6 +72,9 @@ public TokenRevocationResponseGenerator(IReferenceTokenStore referenceTokenStore /// A task that resolves to a indicating whether the token was successfully revoked. public virtual async Task ProcessAsync(TokenRevocationRequestValidationResult validationResult) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Basic, this); + trace?.AddTag(TelemetryConstants.TagConstants.Client, validationResult.Client.ClientId); + var response = new TokenRevocationResponse { Success = false, diff --git a/src/Open.IdentityServer/src/ResponseHandling/Default/UserInfoResponseGenerator.cs b/src/Open.IdentityServer/src/ResponseHandling/Default/UserInfoResponseGenerator.cs index f359e4b22..af1245d7b 100644 --- a/src/Open.IdentityServer/src/ResponseHandling/Default/UserInfoResponseGenerator.cs +++ b/src/Open.IdentityServer/src/ResponseHandling/Default/UserInfoResponseGenerator.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -37,16 +38,23 @@ public class UserInfoResponseGenerator : IUserInfoResponseGenerator /// protected readonly IResourceStore Resources; + /// + /// The telemetry + /// + protected readonly ITelemetryService Telemetry; + /// /// Initializes a new instance of the class. /// /// The profile. /// The resource store. + /// The telemetry. /// The logger. - public UserInfoResponseGenerator(IProfileService profile, IResourceStore resourceStore, ILogger logger) + public UserInfoResponseGenerator(IProfileService profile, IResourceStore resourceStore, ITelemetryService telemetry, ILogger logger) { Profile = profile; Resources = resourceStore; + Telemetry = telemetry; Logger = logger; } @@ -58,6 +66,9 @@ public UserInfoResponseGenerator(IProfileService profile, IResourceStore resourc /// Profile service returned incorrect subject value public virtual async Task> ProcessAsync(UserInfoRequestValidationResult validationResult) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Basic, this); + trace?.AddTag(TelemetryConstants.TagConstants.Client, validationResult.TokenValidationResult.Client); + Logger.LogDebug("Creating userinfo response"); // extract scopes and turn into requested claim types diff --git a/src/Open.IdentityServer/src/Services/Default/BackChannelLogoutHttpClient.cs b/src/Open.IdentityServer/src/Services/Default/BackChannelLogoutHttpClient.cs index a11dd7035..cc98874c5 100644 --- a/src/Open.IdentityServer/src/Services/Default/BackChannelLogoutHttpClient.cs +++ b/src/Open.IdentityServer/src/Services/Default/BackChannelLogoutHttpClient.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -16,16 +17,20 @@ namespace Open.IdentityServer.Services; public class DefaultBackChannelLogoutHttpClient : IBackChannelLogoutHttpClient { private readonly HttpClient _client; + private readonly ITelemetryService _telemetry; private readonly ILogger _logger; /// /// Constructor for BackChannelLogoutHttpClient. /// /// The used to send back-channel logout POST requests. + /// The telemetry service. /// The logger factory used to create a logger for this class. - public DefaultBackChannelLogoutHttpClient(HttpClient client, ILoggerFactory loggerFactory) + public DefaultBackChannelLogoutHttpClient(HttpClient client, ITelemetryService telemetry, + ILoggerFactory loggerFactory) { _client = client; + _telemetry = telemetry; _logger = loggerFactory.CreateLogger(); } @@ -36,6 +41,8 @@ public DefaultBackChannelLogoutHttpClient(HttpClient client, ILoggerFactory logg /// The form-URL-encoded key/value pairs to include in the request body. public async Task PostAsync(string url, Dictionary payload) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + try { var response = await _client.PostAsync(url, new FormUrlEncodedContent(payload)); diff --git a/src/Open.IdentityServer/src/Services/Default/DefaultBackChannelLogoutService.cs b/src/Open.IdentityServer/src/Services/Default/DefaultBackChannelLogoutService.cs index db50b91ea..85495b448 100644 --- a/src/Open.IdentityServer/src/Services/Default/DefaultBackChannelLogoutService.cs +++ b/src/Open.IdentityServer/src/Services/Default/DefaultBackChannelLogoutService.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -42,6 +43,11 @@ public class DefaultBackChannelLogoutService : IBackChannelLogoutService /// HttpClient to make the outbound HTTP calls. /// protected IBackChannelLogoutHttpClient HttpClient { get; } + + /// + /// The telemetry service. + /// + protected ITelemetryService Telemetry { get; } /// /// The logger. @@ -55,12 +61,14 @@ public class DefaultBackChannelLogoutService : IBackChannelLogoutService /// The IdentityServer tools used to issue signed JWTs. /// The service that builds the set of back-channel logout requests for a given logout context. /// The HTTP client used to POST logout tokens to client back-channel URIs. + /// The telemetry service. /// The logger. public DefaultBackChannelLogoutService( TimeProvider clock, IdentityServerTools tools, ILogoutNotificationService logoutNotificationService, IBackChannelLogoutHttpClient backChannelLogoutHttpClient, + ITelemetryService telemetry, ILogger logger) { Clock = clock; @@ -68,11 +76,14 @@ public DefaultBackChannelLogoutService( LogoutNotificationService = logoutNotificationService; HttpClient = backChannelLogoutHttpClient; Logger = logger; + Telemetry = telemetry; } /// public virtual async Task SendLogoutNotificationsAsync(LogoutNotificationContext context) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + var backChannelRequests = await LogoutNotificationService.GetBackChannelLogoutNotificationsAsync(context); if (backChannelRequests.Any()) { diff --git a/src/Open.IdentityServer/src/Services/Default/DefaultClaimsService.cs b/src/Open.IdentityServer/src/Services/Default/DefaultClaimsService.cs index 6248d6c85..e4a4ec3c0 100644 --- a/src/Open.IdentityServer/src/Services/Default/DefaultClaimsService.cs +++ b/src/Open.IdentityServer/src/Services/Default/DefaultClaimsService.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -27,16 +28,26 @@ public class DefaultClaimsService : IClaimsService /// The user service /// protected readonly IProfileService Profile; + + /// + /// The telemetry service + /// + protected readonly ITelemetryService Telemetry; /// /// Initializes a new instance of the class. /// /// The profile service + /// The telemetry service /// The logger - public DefaultClaimsService(IProfileService profile, ILogger logger) + public DefaultClaimsService( + IProfileService profile, + ITelemetryService telemetry, + ILogger logger) { Logger = logger; Profile = profile; + Telemetry = telemetry; } /// @@ -51,9 +62,12 @@ public DefaultClaimsService(IProfileService profile, ILogger public virtual async Task> GetIdentityTokenClaimsAsync(ClaimsPrincipal subject, ResourceValidationResult resources, bool includeAllIdentityClaims, ValidatedRequest request) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + trace + ?.AddTag(TelemetryConstants.TagConstants.Client, request.Client.ClientId) + ?.AddTag(TelemetryConstants.TagConstants.Subject, subject.GetSubjectId()); Logger.LogDebug("Getting claims for identity token for subject: {subject} and client: {clientId}", - subject.GetSubjectId(), - request.Client.ClientId); + subject.GetSubjectId(), request.Client.ClientId); var outputClaims = new List(GetStandardSubjectClaims(subject)); outputClaims.AddRange(GetOptionalClaims(subject)); @@ -111,6 +125,8 @@ public virtual async Task> GetIdentityTokenClaimsAsync(Claims /// public virtual async Task> GetAccessTokenClaimsAsync(ClaimsPrincipal subject, ResourceValidationResult resourceResult, ValidatedRequest request) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + trace?.AddTag(TelemetryConstants.TagConstants.Client, request.Client.ClientId); Logger.LogDebug("Getting claims for access token for client: {clientId}", request.Client.ClientId); var outputClaims = new List diff --git a/src/Open.IdentityServer/src/Services/Default/DefaultConsentService.cs b/src/Open.IdentityServer/src/Services/Default/DefaultConsentService.cs index a3492d4d8..e351c5ed9 100644 --- a/src/Open.IdentityServer/src/Services/Default/DefaultConsentService.cs +++ b/src/Open.IdentityServer/src/Services/Default/DefaultConsentService.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -26,9 +27,14 @@ public class DefaultConsentService : IConsentService protected readonly IUserConsentStore UserConsentStore; /// - /// The clock + /// The clock /// protected readonly TimeProvider Clock; + + /// + /// The telemetry + /// + protected readonly ITelemetryService Telemetry; /// /// The logger @@ -40,12 +46,18 @@ public class DefaultConsentService : IConsentService /// /// The clock. /// The user consent store. + /// The telemetry service. /// The logger. /// store - public DefaultConsentService(TimeProvider clock, IUserConsentStore userConsentStore, ILogger logger) + public DefaultConsentService( + TimeProvider clock, + IUserConsentStore userConsentStore, + ITelemetryService telemetry, + ILogger logger) { Clock = clock; UserConsentStore = userConsentStore; + Telemetry = telemetry; Logger = logger; } @@ -68,6 +80,11 @@ public virtual async Task RequiresConsentAsync(ClaimsPrincipal subject, Cl if (client == null) throw new ArgumentNullException(nameof(client)); if (subject == null) throw new ArgumentNullException(nameof(subject)); + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + trace + ?.AddTag(TelemetryConstants.TagConstants.Client, client.ClientId) + ?.AddTag(TelemetryConstants.TagConstants.Subject, subject.GetSubjectId()); + if (!client.RequireConsent) { Logger.LogDebug("Client is configured to not require consent, no consent is required"); @@ -93,6 +110,7 @@ public virtual async Task RequiresConsentAsync(ClaimsPrincipal subject, Cl } var scopes = parsedScopes.Select(x => x.RawValue).ToArray(); + trace?.AddTag(TelemetryConstants.TagConstants.Scope, string.Join(" ", scopes)); // we always require consent for offline access if // the client has not disabled RequireConsent @@ -156,6 +174,11 @@ public virtual async Task UpdateConsentAsync(ClaimsPrincipal subject, Client cli if (client == null) throw new ArgumentNullException(nameof(client)); if (subject == null) throw new ArgumentNullException(nameof(subject)); + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + trace + ?.AddTag(TelemetryConstants.TagConstants.Client, client.ClientId) + ?.AddTag(TelemetryConstants.TagConstants.Subject, subject.GetSubjectId()); + if (client.AllowRememberConsent) { var subjectId = subject.GetSubjectId(); @@ -164,6 +187,7 @@ public virtual async Task UpdateConsentAsync(ClaimsPrincipal subject, Client cli var scopes = parsedScopes?.Select(x => x.RawValue).ToArray(); if (scopes != null && scopes.Any()) { + trace?.AddTag(TelemetryConstants.TagConstants.Scope, string.Join(" ", scopes)); Logger.LogDebug("Client allows remembering consent, and consent given. Updating consent store for subject: {subject}", subject.GetSubjectId()); var consent = new Consent diff --git a/src/Open.IdentityServer/src/Services/Default/DefaultDeviceFlowCodeService.cs b/src/Open.IdentityServer/src/Services/Default/DefaultDeviceFlowCodeService.cs index c60ce3464..6a4caa614 100644 --- a/src/Open.IdentityServer/src/Services/Default/DefaultDeviceFlowCodeService.cs +++ b/src/Open.IdentityServer/src/Services/Default/DefaultDeviceFlowCodeService.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. using System.Threading.Tasks; @@ -15,17 +16,22 @@ public class DefaultDeviceFlowCodeService : IDeviceFlowCodeService { private readonly IDeviceFlowStore _store; private readonly IHandleGenerationService _handleGenerationService; + private readonly ITelemetryService _telemetry; /// /// Initializes a new instance of the class. /// /// The store. /// The handle generation service. - public DefaultDeviceFlowCodeService(IDeviceFlowStore store, - IHandleGenerationService handleGenerationService) + /// The telemetry service. + public DefaultDeviceFlowCodeService( + IDeviceFlowStore store, + IHandleGenerationService handleGenerationService, + ITelemetryService telemetry) { _store = store; _handleGenerationService = handleGenerationService; + _telemetry = telemetry; } /// @@ -36,6 +42,8 @@ public DefaultDeviceFlowCodeService(IDeviceFlowStore store, /// A task that resolves to the device code handle assigned to the stored authorization. public async Task StoreDeviceAuthorizationAsync(string userCode, DeviceCode data) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + var deviceCode = await _handleGenerationService.GenerateAsync(); await _store.StoreDeviceAuthorizationAsync(deviceCode.Sha256(), userCode.Sha256(), data); @@ -50,6 +58,8 @@ public async Task StoreDeviceAuthorizationAsync(string userCode, DeviceC /// A task that resolves to the associated with , or if not found. public Task FindByUserCodeAsync(string userCode) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + return _store.FindByUserCodeAsync(userCode.Sha256()); } @@ -60,6 +70,8 @@ public Task FindByUserCodeAsync(string userCode) /// A task that resolves to the associated with , or if not found. public Task FindByDeviceCodeAsync(string deviceCode) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + return _store.FindByDeviceCodeAsync(deviceCode.Sha256()); } @@ -70,6 +82,8 @@ public Task FindByDeviceCodeAsync(string deviceCode) /// The data. public Task UpdateByUserCodeAsync(string userCode, DeviceCode data) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + return _store.UpdateByUserCodeAsync(userCode.Sha256(), data); } @@ -79,6 +93,8 @@ public Task UpdateByUserCodeAsync(string userCode, DeviceCode data) /// The device code. public Task RemoveByDeviceCodeAsync(string deviceCode) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + return _store.RemoveByDeviceCodeAsync(deviceCode.Sha256()); } } \ No newline at end of file diff --git a/src/Open.IdentityServer/src/Services/Default/DefaultDeviceFlowInteractionService.cs b/src/Open.IdentityServer/src/Services/Default/DefaultDeviceFlowInteractionService.cs index 4a0d53f7f..841e6a658 100644 --- a/src/Open.IdentityServer/src/Services/Default/DefaultDeviceFlowInteractionService.cs +++ b/src/Open.IdentityServer/src/Services/Default/DefaultDeviceFlowInteractionService.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -18,6 +19,7 @@ internal class DefaultDeviceFlowInteractionService : IDeviceFlowInteractionServi private readonly IDeviceFlowCodeService _devices; private readonly IResourceStore _resourceStore; private readonly IScopeParser _scopeParser; + private readonly ITelemetryService _telemetry; private readonly ILogger _logger; public DefaultDeviceFlowInteractionService( @@ -26,6 +28,7 @@ public DefaultDeviceFlowInteractionService( IDeviceFlowCodeService devices, IResourceStore resourceStore, IScopeParser scopeParser, + ITelemetryService telemetry, ILogger logger) { _clients = clients; @@ -33,11 +36,14 @@ public DefaultDeviceFlowInteractionService( _devices = devices; _resourceStore = resourceStore; _scopeParser = scopeParser; + _telemetry = telemetry; _logger = logger; } public async Task GetAuthorizationContextAsync(string userCode) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + var deviceAuth = await _devices.FindByUserCodeAsync(userCode); if (deviceAuth == null) return null; @@ -58,6 +64,8 @@ public async Task HandleRequestAsync(string userCod { if (userCode == null) throw new ArgumentNullException(nameof(userCode)); if (consent == null) throw new ArgumentNullException(nameof(consent)); + + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); var deviceAuth = await _devices.FindByUserCodeAsync(userCode); if (deviceAuth == null) return LogAndReturnError("Invalid user code", "Device authorization failure - user code is invalid"); diff --git a/src/Open.IdentityServer/src/Services/Default/DefaultIdentityServerInteractionService.cs b/src/Open.IdentityServer/src/Services/Default/DefaultIdentityServerInteractionService.cs index 7a6dee130..d052f4325 100644 --- a/src/Open.IdentityServer/src/Services/Default/DefaultIdentityServerInteractionService.cs +++ b/src/Open.IdentityServer/src/Services/Default/DefaultIdentityServerInteractionService.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -25,6 +26,7 @@ internal class DefaultIdentityServerInteractionService : IIdentityServerInteract private readonly IUserSession _userSession; private readonly ILogger _logger; private readonly ReturnUrlParser _returnUrlParser; + private readonly ITelemetryService _telemetry; public DefaultIdentityServerInteractionService( TimeProvider clock, @@ -35,6 +37,7 @@ public DefaultIdentityServerInteractionService( IPersistedGrantService grants, IUserSession userSession, ReturnUrlParser returnUrlParser, + ITelemetryService telemetry, ILogger logger) { _clock = clock; @@ -45,11 +48,14 @@ public DefaultIdentityServerInteractionService( _grants = grants; _userSession = userSession; _returnUrlParser = returnUrlParser; + _telemetry = telemetry; _logger = logger; } public async Task GetAuthorizationContextAsync(string returnUrl) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + var result = await _returnUrlParser.ParseAsync(returnUrl); if (result != null) @@ -66,6 +72,8 @@ public async Task GetAuthorizationContextAsync(string retu public async Task GetLogoutContextAsync(string logoutId) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + var msg = await _logoutMessageStore.ReadAsync(logoutId); var iframeUrl = await _context.HttpContext.GetIdentityServerSignoutFrameCallbackUrlAsync(msg?.Data); return new LogoutRequest(iframeUrl, msg?.Data); @@ -73,6 +81,8 @@ public async Task GetLogoutContextAsync(string logoutId) public async Task CreateLogoutContextAsync() { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + var user = await _userSession.GetUserAsync(); if (user != null) { @@ -96,6 +106,8 @@ public async Task CreateLogoutContextAsync() public async Task GetErrorContextAsync(string errorId) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + if (errorId != null) { var result = await _errorMessageStore.ReadAsync(errorId); @@ -118,6 +130,8 @@ public async Task GetErrorContextAsync(string errorId) public async Task GrantConsentAsync(AuthorizationRequest request, ConsentResponse consent, string subject = null) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + if (subject == null) { var user = await _userSession.GetUserAsync(); @@ -135,6 +149,8 @@ public async Task GrantConsentAsync(AuthorizationRequest request, ConsentRespons public Task DenyAuthorizationAsync(AuthorizationRequest request, AuthorizationError error, string errorDescription = null) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + var response = new ConsentResponse { Error = error, @@ -145,6 +161,8 @@ public Task DenyAuthorizationAsync(AuthorizationRequest request, AuthorizationEr public bool IsValidReturnUrl(string returnUrl) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + var result = _returnUrlParser.IsValidReturnUrl(returnUrl); if (result) @@ -161,6 +179,8 @@ public bool IsValidReturnUrl(string returnUrl) public async Task> GetAllUserGrantsAsync() { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + var user = await _userSession.GetUserAsync(); if (user != null) { @@ -173,6 +193,8 @@ public async Task> GetAllUserGrantsAsync() public async Task RevokeUserConsentAsync(string clientId) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + var user = await _userSession.GetUserAsync(); if (user != null) { @@ -183,6 +205,8 @@ public async Task RevokeUserConsentAsync(string clientId) public async Task RevokeTokensForCurrentSessionAsync() { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + var user = await _userSession.GetUserAsync(); if (user != null) { diff --git a/src/Open.IdentityServer/src/Services/Default/DefaultJwtRequestUriHttpClient.cs b/src/Open.IdentityServer/src/Services/Default/DefaultJwtRequestUriHttpClient.cs index 8fef34a44..0e9620626 100644 --- a/src/Open.IdentityServer/src/Services/Default/DefaultJwtRequestUriHttpClient.cs +++ b/src/Open.IdentityServer/src/Services/Default/DefaultJwtRequestUriHttpClient.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -18,6 +19,7 @@ public class DefaultJwtRequestUriHttpClient : IJwtRequestUriHttpClient { private readonly HttpClient _client; private readonly IdentityServerOptions _options; + private readonly ITelemetryService _telemetry; private readonly ILogger _logger; /// @@ -25,17 +27,25 @@ public class DefaultJwtRequestUriHttpClient : IJwtRequestUriHttpClient /// /// An HTTP client /// The options. + /// The telemetry service. /// The logger factory - public DefaultJwtRequestUriHttpClient(HttpClient client, IdentityServerOptions options, ILoggerFactory loggerFactory) + public DefaultJwtRequestUriHttpClient( + HttpClient client, + IdentityServerOptions options, + ITelemetryService telemetry, + ILoggerFactory loggerFactory) { _client = client; _options = options; + _telemetry = telemetry; _logger = loggerFactory.CreateLogger(); } /// public async Task GetJwtAsync(string url, Client client) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + var req = new HttpRequestMessage(HttpMethod.Get, url); req.Options.Set(new HttpRequestOptionsKey(IdentityServerConstants.JwtRequestClientKey), client); diff --git a/src/Open.IdentityServer/src/Services/Default/DefaultKeyMaterialService.cs b/src/Open.IdentityServer/src/Services/Default/DefaultKeyMaterialService.cs index 8afa349e1..1b25576d1 100644 --- a/src/Open.IdentityServer/src/Services/Default/DefaultKeyMaterialService.cs +++ b/src/Open.IdentityServer/src/Services/Default/DefaultKeyMaterialService.cs @@ -22,21 +22,30 @@ public class DefaultKeyMaterialService : IKeyMaterialService { private readonly IEnumerable _signingCredentialStores; private readonly IEnumerable _validationKeysStores; + private readonly ITelemetryService _telemetry; /// /// Initializes a new instance of the class. /// /// The validation keys stores. /// The signing credential store. - public DefaultKeyMaterialService(IEnumerable validationKeysStores, IEnumerable signingCredentialStores) + /// The telemetry service. + public DefaultKeyMaterialService( + IEnumerable validationKeysStores, + IEnumerable signingCredentialStores, + ITelemetryService telemetry) { _signingCredentialStores = signingCredentialStores; + _telemetry = telemetry; _validationKeysStores = validationKeysStores; } /// public async Task GetSigningCredentialsAsync(IEnumerable allowedAlgorithms = null) { + using var trace = _telemetry.Trace( + TelemetryConstants.TraceCategories.Services, this); + if (_signingCredentialStores.Any()) { if (allowedAlgorithms.IsNullOrEmpty()) @@ -59,6 +68,9 @@ public async Task GetSigningCredentialsAsync(IEnumerable public async Task> GetAllSigningCredentialsAsync() { + using var trace = _telemetry.Trace( + TelemetryConstants.TraceCategories.Services, this); + var credentials = new List(); foreach (var store in _signingCredentialStores) @@ -77,6 +89,9 @@ public async Task> GetAllSigningCredentialsAsync /// public async Task> GetValidationKeysAsync() { + using var trace = _telemetry.Trace( + TelemetryConstants.TraceCategories.Services, this); + var keys = new List(); foreach (var store in _validationKeysStores) diff --git a/src/Open.IdentityServer/src/Services/Default/DefaultPersistedGrantService.cs b/src/Open.IdentityServer/src/Services/Default/DefaultPersistedGrantService.cs index df6988ea3..873711cd5 100644 --- a/src/Open.IdentityServer/src/Services/Default/DefaultPersistedGrantService.cs +++ b/src/Open.IdentityServer/src/Services/Default/DefaultPersistedGrantService.cs @@ -23,27 +23,34 @@ public class DefaultPersistedGrantService : IPersistedGrantService private readonly ILogger _logger; private readonly IPersistedGrantStore _store; private readonly IPersistentGrantSerializer _serializer; + private readonly ITelemetryService _telemetry; /// /// Initializes a new instance of the class. /// /// The store. /// The serializer. + /// The telemetry. /// The logger. - public DefaultPersistedGrantService(IPersistedGrantStore store, + public DefaultPersistedGrantService( + IPersistedGrantStore store, IPersistentGrantSerializer serializer, + ITelemetryService telemetry, ILogger logger) { _store = store; _serializer = serializer; _logger = logger; + _telemetry = telemetry; } /// public async Task> GetAllGrantsAsync(string subjectId) { if (String.IsNullOrWhiteSpace(subjectId)) throw new ArgumentNullException(nameof(subjectId)); - + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + trace?.AddTag(TelemetryConstants.TagConstants.Subject, subjectId); + var grants = (await _store.GetAllAsync(new PersistedGrantFilter { SubjectId = subjectId })).ToArray(); try @@ -164,7 +171,9 @@ private IEnumerable Join(IEnumerable first, IEnumerable sec public Task RemoveAllGrantsAsync(string subjectId, string? clientId = null, string? sessionId = null) { if (String.IsNullOrWhiteSpace(subjectId)) throw new ArgumentNullException(nameof(subjectId)); - + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + trace?.AddTag(TelemetryConstants.TagConstants.Subject, subjectId); + return _store.RemoveAllAsync(new PersistedGrantFilter { SubjectId = subjectId, ClientId = clientId, diff --git a/src/Open.IdentityServer/src/Services/Default/DefaultProfileService.cs b/src/Open.IdentityServer/src/Services/Default/DefaultProfileService.cs index 1359d2582..7af4c7658 100644 --- a/src/Open.IdentityServer/src/Services/Default/DefaultProfileService.cs +++ b/src/Open.IdentityServer/src/Services/Default/DefaultProfileService.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -15,6 +16,11 @@ namespace Open.IdentityServer.Services; /// public class DefaultProfileService : IProfileService { + /// + /// The telemetry + /// + protected readonly ITelemetryService Telemetry; + /// /// The logger /// @@ -23,9 +29,11 @@ public class DefaultProfileService : IProfileService /// /// Initializes a new instance of the class. /// + /// The telemetry. /// The logger. - public DefaultProfileService(ILogger logger) + public DefaultProfileService(ITelemetryService telemetry, ILogger logger) { + Telemetry = telemetry; Logger = logger; } @@ -35,6 +43,8 @@ public DefaultProfileService(ILogger logger) /// The context. public virtual Task GetProfileDataAsync(ProfileDataRequestContext context) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + context.LogProfileRequest(Logger); context.AddRequestedClaims(context.Subject.Claims); context.LogIssuedClaims(Logger); @@ -49,6 +59,7 @@ public virtual Task GetProfileDataAsync(ProfileDataRequestContext context) /// The context. public virtual Task IsActiveAsync(IsActiveContext context) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); Logger.LogDebug("IsActive called from: {caller}", context.Caller); context.IsActive = true; diff --git a/src/Open.IdentityServer/src/Services/Default/DefaultRefreshTokenService.cs b/src/Open.IdentityServer/src/Services/Default/DefaultRefreshTokenService.cs index 58a23ea42..0d121715c 100644 --- a/src/Open.IdentityServer/src/Services/Default/DefaultRefreshTokenService.cs +++ b/src/Open.IdentityServer/src/Services/Default/DefaultRefreshTokenService.cs @@ -37,6 +37,11 @@ public class DefaultRefreshTokenService : IRefreshTokenService /// The clock /// protected TimeProvider Clock { get; } + + /// + /// The telemetry service + /// + protected ITelemetryService Telemetry { get; } /// /// Initializes a new instance of the class. @@ -44,14 +49,19 @@ public class DefaultRefreshTokenService : IRefreshTokenService /// The refresh token store /// The profile service used to check whether the token subject is still active. /// The clock + /// The telemetry /// The logger - public DefaultRefreshTokenService(IRefreshTokenStore refreshTokenStore, IProfileService profile, + public DefaultRefreshTokenService( + IRefreshTokenStore refreshTokenStore, + IProfileService profile, TimeProvider clock, + ITelemetryService telemetry, ILogger logger) { RefreshTokenStore = refreshTokenStore; Profile = profile; Clock = clock; + Telemetry = telemetry; Logger = logger; } @@ -64,6 +74,9 @@ public DefaultRefreshTokenService(IRefreshTokenStore refreshTokenStore, IProfile /// A task that resolves to a indicating whether the refresh token is valid for the specified client, including the token and client if successful. public virtual async Task ValidateRefreshTokenAsync(string tokenHandle, Client client) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + trace?.AddTag(TelemetryConstants.TagConstants.Client, client.ClientId); + var invalidGrant = new TokenValidationResult { IsError = true, Error = OidcConstants.TokenErrors.InvalidGrant @@ -167,6 +180,10 @@ protected virtual Task AcceptConsumedTokenAsync(RefreshToken refreshToken) /// public virtual async Task CreateRefreshTokenAsync(RefreshTokenCreationRequest request) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + trace?.AddTag(TelemetryConstants.TagConstants.Client, request.Client.ClientId); + trace?.AddTag(TelemetryConstants.TagConstants.Subject, request.Subject.GetSubjectId()); + Logger.LogDebug("Creating refresh token"); int lifetime; @@ -225,6 +242,10 @@ public virtual async Task CreateRefreshTokenAsync(RefreshTokenCreationRe public virtual async Task UpdateRefreshTokenAsync(string handle, RefreshToken refreshToken, Client client) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + trace?.AddTag(TelemetryConstants.TagConstants.Client, client.ClientId); + if (refreshToken.ClientId != null) trace?.AddTag(TelemetryConstants.TagConstants.Subject, refreshToken.SubjectId); + Logger.LogDebug("Updating refresh token"); bool needsCreate = false; diff --git a/src/Open.IdentityServer/src/Services/Default/DefaultReplayCache.cs b/src/Open.IdentityServer/src/Services/Default/DefaultReplayCache.cs index eda2770ba..a36364ec6 100644 --- a/src/Open.IdentityServer/src/Services/Default/DefaultReplayCache.cs +++ b/src/Open.IdentityServer/src/Services/Default/DefaultReplayCache.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. using System; @@ -15,19 +16,24 @@ public class DefaultReplayCache : IReplayCache private const string Prefix = nameof(DefaultReplayCache) + ":"; private readonly IDistributedCache _cache; - + private readonly ITelemetryService _telemetry; + /// /// Initializes a new instance of the class. /// /// The distributed cache used to store and look up replay-detection entries. - public DefaultReplayCache(IDistributedCache cache) + /// The telemetry service + public DefaultReplayCache(IDistributedCache cache, ITelemetryService telemetry) { _cache = cache; + _telemetry = telemetry; } /// public async Task AddAsync(string purpose, string handle, DateTimeOffset expiration) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + var options = new DistributedCacheEntryOptions { AbsoluteExpiration = expiration @@ -39,6 +45,8 @@ public async Task AddAsync(string purpose, string handle, DateTimeOffset expirat /// public async Task ExistsAsync(string purpose, string handle) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + return (await _cache.GetAsync(Prefix + purpose + handle, default)) != null; } } \ No newline at end of file diff --git a/src/Open.IdentityServer/src/Services/Default/DefaultTelemetryService.cs b/src/Open.IdentityServer/src/Services/Default/DefaultTelemetryService.cs new file mode 100644 index 000000000..c535f4c49 --- /dev/null +++ b/src/Open.IdentityServer/src/Services/Default/DefaultTelemetryService.cs @@ -0,0 +1,367 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Metrics; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace Open.IdentityServer.Services; + +/// +public class DefaultTelemetryService : ITelemetryService, IDisposable +{ + private readonly Meter _meter; + + private readonly Counter _operation; + private readonly UpDownCounter _activeRequests; + private readonly Counter _apiSecretValidation; + private readonly Counter _backchannelAuthentication; + private readonly Counter _clientConfigValidation; + private readonly Counter _clientSecretValidation; + private readonly Counter _deviceAuthentication; + private readonly Counter _introspection; + private readonly Counter _resourceOwnerAuthentication; + private readonly Counter _tokenIssued; + private readonly Counter _revocation; + private readonly Counter _pushedAuthorizationRequest; + + private Dictionary _activitySources; + + /// + /// Initializes a new instance of the class. + /// + public DefaultTelemetryService() + { + Version version = GetType().Assembly.GetName().Version ?? new Version(1, 0, 0); + var versionString = $"{version.Major}.{version.Minor}.{version.Build}"; + _meter = new Meter(TelemetryConstants.MetricsConstants.MeterName, versionString); + + _operation = _meter.CreateCounter(TelemetryConstants.MetricsConstants.OperationCounterName); + _activeRequests = _meter.CreateUpDownCounter(TelemetryConstants.MetricsConstants.ActiveRequestsCounterName); + _apiSecretValidation = _meter.CreateCounter(TelemetryConstants.MetricsConstants.ApiSecretValidationCounterName); + _backchannelAuthentication = _meter.CreateCounter(TelemetryConstants.MetricsConstants.BackChannelAuthenticationCounterName); + _clientConfigValidation = _meter.CreateCounter(TelemetryConstants.MetricsConstants.ClientConfigValidationCounterName); + _clientSecretValidation = _meter.CreateCounter(TelemetryConstants.MetricsConstants.ClientSecretValidationCounterName); + _deviceAuthentication = _meter.CreateCounter(TelemetryConstants.MetricsConstants.DeviceAuthenticationCounterName); + _introspection = _meter.CreateCounter(TelemetryConstants.MetricsConstants.IntrospectionCounterName); + _resourceOwnerAuthentication = _meter.CreateCounter(TelemetryConstants.MetricsConstants.ResourceOwnerAuthenticationCounterName); + _tokenIssued = _meter.CreateCounter(TelemetryConstants.MetricsConstants.TokenIssuedCounterName); + _revocation = _meter.CreateCounter(TelemetryConstants.MetricsConstants.RevocationCounterName); + _pushedAuthorizationRequest = _meter.CreateCounter(TelemetryConstants.MetricsConstants.PushedAuthorizationRequestCounterName); + + _activitySources = new() + { + { TelemetryConstants.TraceCategories.Basic, new ActivitySource(TelemetryConstants.TraceCategories.Basic, versionString) }, + { TelemetryConstants.TraceCategories.Cache, new ActivitySource(TelemetryConstants.TraceCategories.Cache, versionString) }, + { TelemetryConstants.TraceCategories.Services, new ActivitySource(TelemetryConstants.TraceCategories.Services, versionString) }, + { TelemetryConstants.TraceCategories.Stores, new ActivitySource(TelemetryConstants.TraceCategories.Stores, versionString) }, + { TelemetryConstants.TraceCategories.Validation, new ActivitySource(TelemetryConstants.TraceCategories.Validation, versionString) } + }; + } + + /// + public void CountInternalError(string error) + { + TagList tags = new TagList + { + { TelemetryConstants.TagConstants.Result, TelemetryConstants.TagConstants.InternalError }, + { TelemetryConstants.TagConstants.Error, error } + }; + + _operation.Add(delta: 1, tags); + } + + /// + public IDisposable BeginActiveRequest(string endpoint, string path) + { + var tags = new TagList + { + { TelemetryConstants.TagConstants.Endpoint, endpoint }, + { TelemetryConstants.TagConstants.Path, path } + }; + + _activeRequests.Add(delta: 1, tags); + + return new ActionDisposable(() => + { + _activeRequests.Add(delta: -1, tags); + }); + } + + /// + public void CountApiSecretValidation(string client, string authMethod = null, string error = null) + { + var tags = new TagList() + { + { TelemetryConstants.TagConstants.Api, client }, + }; + + if (authMethod != null) + { + tags.Add(TelemetryConstants.TagConstants.AuthMethod, authMethod); + } + + if (error != null) + { + tags.Add(TelemetryConstants.TagConstants.Error, error); + } + + _apiSecretValidation.Add(delta: 1, tags); + CountOperationSuccessOrFailure(client, error); + } + + /// + public void CountBackchannelAuthentication(string client, string error = null) + { + var tags = CreateClientAndErrorTagList(client, error); + + _backchannelAuthentication.Add(delta: 1, tags); + CountOperationSuccessOrFailure(client, error); + } + + private static TagList CreateClientAndErrorTagList(string client, string error) + { + var tags = new TagList + { + { TelemetryConstants.TagConstants.Client, client }, + }; + + if (error != null) + { + tags.Add(TelemetryConstants.TagConstants.Error, error); + } + + return tags; + } + + /// + public void CountClientConfigValidation(string client, string error = null) + { + var tags = CreateClientAndErrorTagList(client, error); + + _clientConfigValidation.Add(delta: 1, tags); + + CountOperationSuccessOrFailure(client, error); + } + + /// + public void CountClientSecretValidation(string client, string authMethod = null, string error = null) + { + var tags = new TagList + { + { TelemetryConstants.TagConstants.Client, client }, + }; + + if (authMethod != null) + { + tags.Add(TelemetryConstants.TagConstants.AuthMethod, authMethod); + } + + if (error != null) + { + tags.Add(TelemetryConstants.TagConstants.Error, error); + } + + _clientSecretValidation.Add(delta: 1, tags); + + CountOperationSuccessOrFailure(client, error); + } + + /// + public void CountResourceOwnerAuthentication(string client, string error = null) + { + var tags = CreateClientAndErrorTagList(client, error); + + _resourceOwnerAuthentication.Add(delta: 1, tags); + + CountOperationSuccessOrFailure(client, error); + } + + /// + public void CountDeviceAuthentication(string client, string error = null) + { + var tags = CreateClientAndErrorTagList(client, error); + + _deviceAuthentication.Add(delta: 1, tags); + + CountOperationSuccessOrFailure(client, error); + } + + /// + public void CountTokenIntrospection(string caller, bool? active = null, string error = null) + { + var tags = new TagList + { + { TelemetryConstants.TagConstants.Caller, caller }, + }; + + if (active.HasValue) + { + tags.Add(TelemetryConstants.TagConstants.Active, active.Value); + } + + if (error != null) + { + tags.Add(TelemetryConstants.TagConstants.Error, error); + } + + _introspection.Add(delta: 1, tags); + + CountOperationSuccessOrFailure(caller, error); + } + + /// + public void CountPushedAuthorizationRequest(string client, string error = null) + { + var tags = CreateClientAndErrorTagList(client, error); + + _pushedAuthorizationRequest.Add(delta: 1, tags); + + CountOperationSuccessOrFailure(client, error); + } + + /// + public void CountTokenRevocation(string client,string error = null) + { + var tags = CreateClientAndErrorTagList(client, error); + + _revocation.Add(delta: 1, tags); + + CountOperationSuccessOrFailure(client, error); + } + + /// + public void CountTokenIssued(string client, string grantType, string error = null) + { + var tags = new TagList + { + { TelemetryConstants.TagConstants.Client, client }, + { TelemetryConstants.TagConstants.GrantType, grantType }, + }; + + if (error != null) + { + tags.Add(TelemetryConstants.TagConstants.Error, error); + } + + _tokenIssued.Add(delta: 1, tags); + + CountOperationSuccessOrFailure(client, error); + } + + /// + public ITrace Trace(string category, string activityName) + { + if (_activitySources.TryGetValue(category, out var activitySource)) + { + return new TraceActivityAdapter( + activitySource.StartActivity(activityName) + ); + } + + return null; + } + + /// + public ITrace Trace(string category, object caller, [CallerMemberName] string callingMethod = null) + { + return Trace(category, activityName: $"{caller.GetType().FullName}.{callingMethod}"); + } + + private void CountOperationSuccessOrFailure(string clientId, string error = null) + { + if (error == null) + { + CountOperationSucceeded(clientId); + return; + } + + CountOperationFailed(clientId, error); + } + + private void CountOperationSucceeded(string clientId) + { + TagList tags = new TagList + { + { TelemetryConstants.TagConstants.Result, TelemetryConstants.TagConstants.Success }, + { TelemetryConstants.TagConstants.Client, clientId } + }; + + _operation.Add(delta: 1, tags); + } + + private void CountOperationFailed(string clientId, string error) + { + TagList tags = new TagList + { + { TelemetryConstants.TagConstants.Result, TelemetryConstants.TagConstants.Error }, + { TelemetryConstants.TagConstants.Client, clientId }, + { TelemetryConstants.TagConstants.Error, error } + }; + + _operation.Add(delta: 1, tags); + } + + private class ActionDisposable : IDisposable + { + private Action _onDispose; + + internal ActionDisposable(Action onDispose) + { + _onDispose = onDispose; + } + + public void Dispose() + { + _onDispose?.Invoke(); + _onDispose = null; + } + } + + private class TraceActivityAdapter : ITrace + { + private Activity _activity; + + public TraceActivityAdapter(Activity activity) + { + _activity = activity; + } + + public void Dispose() + { + _activity?.Dispose(); + _activity = null; + } + + public ITrace AddTag(string key, string value) + { + _activity?.AddTag(key, value); + return this; + } + + public ITrace AddTag(string key, object value) + { + _activity?.AddTag(key, value); + return this; + } + + public ITrace AddException(Exception exception) + { + _activity?.AddException(exception); + return this; + } + } + + /// + public void Dispose() + { + _meter?.Dispose(); + foreach (var keyValuePair in _activitySources) + { + keyValuePair.Value.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/src/Services/Default/DefaultTokenCreationService.cs b/src/Open.IdentityServer/src/Services/Default/DefaultTokenCreationService.cs index ae7d1b45e..621a9e041 100644 --- a/src/Open.IdentityServer/src/Services/Default/DefaultTokenCreationService.cs +++ b/src/Open.IdentityServer/src/Services/Default/DefaultTokenCreationService.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -40,6 +41,11 @@ public class DefaultTokenCreationService : ITokenCreationService /// The options /// protected readonly IdentityServerOptions Options; + + /// + /// The telemetry + /// + protected readonly ITelemetryService Telemetry; /// /// Initializes a new instance of the class. @@ -47,17 +53,20 @@ public class DefaultTokenCreationService : ITokenCreationService /// The options. /// The keys. /// The options. + /// /// The logger. public DefaultTokenCreationService( TimeProvider clock, IKeyMaterialService keys, IdentityServerOptions options, + ITelemetryService telemetry, ILogger logger) { Clock = clock; Keys = keys; Options = options; Logger = logger; + Telemetry = telemetry; } /// @@ -69,6 +78,8 @@ public DefaultTokenCreationService( /// public virtual async Task CreateTokenAsync(Token token) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + var header = await CreateHeaderAsync(token); var payload = await CreatePayloadAsync(token); diff --git a/src/Open.IdentityServer/src/Services/Default/DefaultTokenService.cs b/src/Open.IdentityServer/src/Services/Default/DefaultTokenService.cs index 1936807f7..5325aff4b 100644 --- a/src/Open.IdentityServer/src/Services/Default/DefaultTokenService.cs +++ b/src/Open.IdentityServer/src/Services/Default/DefaultTokenService.cs @@ -61,6 +61,11 @@ public class DefaultTokenService : ITokenService /// The IdentityServer options /// protected readonly IdentityServerOptions Options; + + /// + /// The telemetry service + /// + protected readonly ITelemetryService Telemetry; /// /// Initializes a new instance of the class. @@ -72,6 +77,7 @@ public class DefaultTokenService : ITokenService /// The clock. /// The key material service used to retrieve signing credentials for token creation. /// The IdentityServer options + /// The telemetry. /// The logger. public DefaultTokenService( IClaimsService claimsProvider, @@ -81,6 +87,7 @@ public DefaultTokenService( TimeProvider clock, IKeyMaterialService keyMaterialService, IdentityServerOptions options, + ITelemetryService telemetry, ILogger logger) { ContextAccessor = contextAccessor; @@ -91,6 +98,7 @@ public DefaultTokenService( KeyMaterialService = keyMaterialService; Options = options; Logger = logger; + Telemetry = telemetry; } /// @@ -102,6 +110,7 @@ public DefaultTokenService( /// public virtual async Task CreateIdentityTokenAsync(TokenCreationRequest request) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); Logger.LogTrace("Creating identity token"); request.Validate(); @@ -183,6 +192,7 @@ public virtual async Task CreateIdentityTokenAsync(TokenCreationRequest r /// public virtual async Task CreateAccessTokenAsync(TokenCreationRequest request) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); Logger.LogTrace("Creating access token"); request.Validate(); @@ -263,6 +273,7 @@ public virtual async Task CreateAccessTokenAsync(TokenCreationRequest req /// Invalid token type. public virtual async Task CreateSecurityTokenAsync(Token token) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); string tokenResult; if (token.Type == OidcConstants.TokenTypes.AccessToken) diff --git a/src/Open.IdentityServer/src/Services/Default/DefaultUserSession.cs b/src/Open.IdentityServer/src/Services/Default/DefaultUserSession.cs index 62d41260a..d31704648 100644 --- a/src/Open.IdentityServer/src/Services/Default/DefaultUserSession.cs +++ b/src/Open.IdentityServer/src/Services/Default/DefaultUserSession.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. using System; diff --git a/src/Open.IdentityServer/src/Services/Default/DistributedDeviceFlowThrottlingService.cs b/src/Open.IdentityServer/src/Services/Default/DistributedDeviceFlowThrottlingService.cs index 7ff47c89c..c619901c0 100644 --- a/src/Open.IdentityServer/src/Services/Default/DistributedDeviceFlowThrottlingService.cs +++ b/src/Open.IdentityServer/src/Services/Default/DistributedDeviceFlowThrottlingService.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -19,6 +20,7 @@ public class DistributedDeviceFlowThrottlingService : IDeviceFlowThrottlingServi private readonly IDistributedCache _cache; private readonly TimeProvider _clock; private readonly IdentityServerOptions _options; + private readonly ITelemetryService _telemetry; private const string KeyPrefix = "devicecode_"; @@ -28,14 +30,17 @@ public class DistributedDeviceFlowThrottlingService : IDeviceFlowThrottlingServi /// The cache. /// The clock. /// The options. + /// The telemetry public DistributedDeviceFlowThrottlingService( IDistributedCache cache, TimeProvider clock, - IdentityServerOptions options) + IdentityServerOptions options, + ITelemetryService telemetry) { _cache = cache; _clock = clock; _options = options; + _telemetry = telemetry; } /// @@ -48,7 +53,8 @@ public DistributedDeviceFlowThrottlingService( public async Task ShouldSlowDown(string deviceCode, DeviceCode details) { if (deviceCode == null) throw new ArgumentNullException(nameof(deviceCode)); - + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + var key = KeyPrefix + deviceCode; var options = new DistributedCacheEntryOptions {AbsoluteExpiration = _clock.GetUtcNow().AddSeconds(details.Lifetime)}; diff --git a/src/Open.IdentityServer/src/Services/Default/LogoutNotificationService.cs b/src/Open.IdentityServer/src/Services/Default/LogoutNotificationService.cs index 05c30fa24..f7ae784a3 100644 --- a/src/Open.IdentityServer/src/Services/Default/LogoutNotificationService.cs +++ b/src/Open.IdentityServer/src/Services/Default/LogoutNotificationService.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. using Open.IdentityServer.Extensions; @@ -19,25 +20,31 @@ public class LogoutNotificationService : ILogoutNotificationService { private readonly IClientStore _clientStore; private readonly IHttpContextAccessor _httpContextAccessor; + private readonly ITelemetryService _telemetry; private readonly ILogger _logger; - - + /// /// Ctor. /// public LogoutNotificationService( IClientStore clientStore, IHttpContextAccessor httpContextAccessor, + ITelemetryService telemetry, ILogger logger) { _clientStore = clientStore; _httpContextAccessor = httpContextAccessor; + _telemetry = telemetry; _logger = logger; } /// public async Task> GetFrontChannelLogoutNotificationsUrlsAsync(LogoutNotificationContext context) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + trace?.AddTag(TelemetryConstants.TagConstants.Subject, context.SubjectId); + trace?.AddTag(TelemetryConstants.TagConstants.Session, context.SessionId); + var frontChannelUrls = new List(); foreach (var clientId in context.ClientIds) { @@ -83,6 +90,10 @@ public async Task> GetFrontChannelLogoutNotificationsUrlsAsy /// public async Task> GetBackChannelLogoutNotificationsAsync(LogoutNotificationContext context) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + trace?.AddTag(TelemetryConstants.TagConstants.Subject, context.SubjectId); + trace?.AddTag(TelemetryConstants.TagConstants.Session, context.SessionId); + var backChannelLogouts = new List(); foreach (var clientId in context.ClientIds) { diff --git a/src/Open.IdentityServer/src/Services/Default/OidcReturnUrlParser.cs b/src/Open.IdentityServer/src/Services/Default/OidcReturnUrlParser.cs index b76f75925..9db99cc82 100644 --- a/src/Open.IdentityServer/src/Services/Default/OidcReturnUrlParser.cs +++ b/src/Open.IdentityServer/src/Services/Default/OidcReturnUrlParser.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -19,21 +20,26 @@ internal class OidcReturnUrlParser : IReturnUrlParser private readonly IUserSession _userSession; private readonly ILogger _logger; private readonly IAuthorizationParametersMessageStore _authorizationParametersMessageStore; + private readonly ITelemetryService _telemetry; public OidcReturnUrlParser( IAuthorizeRequestValidator validator, IUserSession userSession, ILogger logger, + ITelemetryService telemetry, IAuthorizationParametersMessageStore authorizationParametersMessageStore = null) { _validator = validator; _userSession = userSession; _logger = logger; + _telemetry = telemetry; _authorizationParametersMessageStore = authorizationParametersMessageStore; } public async Task ParseAsync(string returnUrl) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + if (IsValidReturnUrl(returnUrl)) { var parameters = returnUrl.ReadQueryStringAsNameValueCollection(); @@ -59,6 +65,8 @@ public async Task ParseAsync(string returnUrl) public bool IsValidReturnUrl(string returnUrl) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + if (returnUrl.IsLocalUrl()) { var index = returnUrl.IndexOf('?'); diff --git a/src/Open.IdentityServer/src/Services/Default/ReturnUrlParser.cs b/src/Open.IdentityServer/src/Services/Default/ReturnUrlParser.cs index cda7adcf2..51e8a75da 100644 --- a/src/Open.IdentityServer/src/Services/Default/ReturnUrlParser.cs +++ b/src/Open.IdentityServer/src/Services/Default/ReturnUrlParser.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -14,14 +15,17 @@ namespace Open.IdentityServer.Services; public class ReturnUrlParser { private readonly IEnumerable _parsers; + private readonly ITelemetryService _telemetry; /// /// Initializes a new instance of the class. /// /// The parsers. - public ReturnUrlParser(IEnumerable parsers) + /// The telemetry. + public ReturnUrlParser(IEnumerable parsers, ITelemetryService telemetry) { _parsers = parsers; + _telemetry = telemetry; } /// @@ -31,6 +35,8 @@ public ReturnUrlParser(IEnumerable parsers) /// public virtual async Task ParseAsync(string returnUrl) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + foreach (var parser in _parsers) { var result = await parser.ParseAsync(returnUrl); @@ -52,6 +58,8 @@ public virtual async Task ParseAsync(string returnUrl) /// public virtual bool IsValidReturnUrl(string returnUrl) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + foreach (var parser in _parsers) { if (parser.IsValidReturnUrl(returnUrl)) diff --git a/src/Open.IdentityServer/src/Services/InMemory/InMemoryCorsPolicyService.cs b/src/Open.IdentityServer/src/Services/InMemory/InMemoryCorsPolicyService.cs index d83321b28..57f9d83ad 100644 --- a/src/Open.IdentityServer/src/Services/InMemory/InMemoryCorsPolicyService.cs +++ b/src/Open.IdentityServer/src/Services/InMemory/InMemoryCorsPolicyService.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -21,19 +22,27 @@ public class InMemoryCorsPolicyService : ICorsPolicyService /// Logger /// protected readonly ILogger Logger; + /// /// Clients applications list /// protected readonly IEnumerable Clients; + + /// + /// The telemetry + /// + protected readonly ITelemetryService Telemetry; /// /// Initializes a new instance of the class. /// /// The logger /// The clients. - public InMemoryCorsPolicyService(ILogger logger, IEnumerable clients) + /// The telemetry service. + public InMemoryCorsPolicyService(ILogger logger, IEnumerable clients, ITelemetryService telemetry) { Logger = logger; + Telemetry = telemetry; Clients = clients ?? Enumerable.Empty(); } @@ -46,6 +55,8 @@ public InMemoryCorsPolicyService(ILogger logger, IEnu /// public virtual Task IsOriginAllowedAsync(string origin) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Services, this); + var query = from client in Clients from url in client.AllowedCorsOrigins diff --git a/src/Open.IdentityServer/src/Stores/Caching/CachingClientStore.cs b/src/Open.IdentityServer/src/Stores/Caching/CachingClientStore.cs index 28ba62188..48dab9e87 100644 --- a/src/Open.IdentityServer/src/Stores/Caching/CachingClientStore.cs +++ b/src/Open.IdentityServer/src/Stores/Caching/CachingClientStore.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -21,6 +22,7 @@ public class CachingClientStore : IClientStore { private readonly IdentityServerOptions _options; private readonly ICache _cache; + private readonly ITelemetryService _telemetry; private readonly IClientStore _inner; private readonly ILogger _logger; @@ -30,12 +32,19 @@ public class CachingClientStore : IClientStore /// The options. /// The inner. /// The cache. + /// The telemetry. /// The logger. - public CachingClientStore(IdentityServerOptions options, T inner, ICache cache, ILogger> logger) + public CachingClientStore( + IdentityServerOptions options, + T inner, + ICache cache, + ITelemetryService telemetry, + ILogger> logger) { _options = options; _inner = inner; _cache = cache; + _telemetry = telemetry; _logger = logger; } @@ -48,6 +57,9 @@ public CachingClientStore(IdentityServerOptions options, T inner, ICache /// public async Task FindClientByIdAsync(string clientId) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Cache, this); + trace?.AddTag(TelemetryConstants.TagConstants.Client, clientId); + var client = await _cache.GetAsync(clientId, _options.Caching.ClientStoreExpiration, async () => await _inner.FindClientByIdAsync(clientId), diff --git a/src/Open.IdentityServer/src/Stores/Caching/CachingResourceStore.cs b/src/Open.IdentityServer/src/Stores/Caching/CachingResourceStore.cs index dad646232..66ad7960e 100644 --- a/src/Open.IdentityServer/src/Stores/Caching/CachingResourceStore.cs +++ b/src/Open.IdentityServer/src/Stores/Caching/CachingResourceStore.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -32,6 +33,7 @@ public class CachingResourceStore : IResourceStore private readonly ICache _allCache; private readonly IResourceStore _inner; + private readonly ITelemetryService _telemetry; private readonly ILogger _logger; /// @@ -44,6 +46,7 @@ public class CachingResourceStore : IResourceStore /// The API cache. /// The cache for API scope lookups by scope name. /// All cache. + /// The telemetry /// The logger. public CachingResourceStore(IdentityServerOptions options, T inner, ICache> identityCache, @@ -51,6 +54,7 @@ public CachingResourceStore(IdentityServerOptions options, T inner, ICache> apisCache, ICache> scopeCache, ICache allCache, + ITelemetryService telemetry, ILogger> logger) { _options = options; @@ -60,6 +64,7 @@ public CachingResourceStore(IdentityServerOptions options, T inner, _apiResourceCache = apisCache; _apiScopeCache = scopeCache; _allCache = allCache; + _telemetry = telemetry; _logger = logger; } @@ -72,6 +77,8 @@ private string GetKey(IEnumerable names) /// public async Task GetAllResourcesAsync() { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Cache, this); + var key = AllKey; var all = await _allCache.GetAsync(key, @@ -85,6 +92,9 @@ public async Task GetAllResourcesAsync() /// public async Task> FindApiResourcesByNameAsync(IEnumerable apiResourceNames) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Cache, this); + trace?.AddTag(TelemetryConstants.TagConstants.Api, GetKey(apiResourceNames)); + var key = GetKey(apiResourceNames); var apis = await _apiResourceCache.GetAsync(key, @@ -98,6 +108,9 @@ public async Task> FindApiResourcesByNameAsync(IEnumera /// public async Task> FindIdentityResourcesByScopeNameAsync(IEnumerable names) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Cache, this); + trace?.AddTag(TelemetryConstants.TagConstants.Scope, GetKey(names)); + var key = GetKey(names); var identities = await _identityCache.GetAsync(key, @@ -111,6 +124,9 @@ public async Task> FindIdentityResourcesByScopeNam /// public async Task> FindApiResourcesByScopeNameAsync(IEnumerable names) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Cache, this); + trace?.AddTag(TelemetryConstants.TagConstants.Scope, GetKey(names)); + var key = GetKey(names); var apis = await _apiByScopeCache.GetAsync(key, @@ -124,6 +140,9 @@ public async Task> FindApiResourcesByScopeNameAsync(IEn /// public async Task> FindApiScopesByNameAsync(IEnumerable scopeNames) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Cache, this); + trace?.AddTag(TelemetryConstants.TagConstants.Scope, GetKey(scopeNames)); + var key = GetKey(scopeNames); var apis = await _apiScopeCache.GetAsync(key, diff --git a/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerSigningCredentialStore.cs b/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerSigningCredentialStore.cs index 253bdbb71..3dd4243a7 100644 --- a/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerSigningCredentialStore.cs +++ b/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerSigningCredentialStore.cs @@ -7,6 +7,7 @@ using Microsoft.IdentityModel.Tokens; using Open.IdentityServer.Configuration; using Open.IdentityServer.DataProtection; +using Open.IdentityServer.Services; namespace Open.IdentityServer.Stores; @@ -17,22 +18,25 @@ namespace Open.IdentityServer.Stores; /// Converter used to decrypt and convert data-protected key material into usable credentials. /// Time provider implementation /// Compatibility key store options +/// The telemetry service public class IdentityServerSigningCredentialStore( IIdentityServerKeyStore identityServerKeyStore, DataProtectedIdentityServerKeyMaterialConverter dataProtectedIdentityServerKeyMaterialConverter, TimeProvider timeProvider, - CompatibilityKeyStoreOptions options + CompatibilityKeyStoreOptions options, + ITelemetryService telemetry ): IdentityServerSigningKeyStore(identityServerKeyStore, dataProtectedIdentityServerKeyMaterialConverter, timeProvider, options), ISigningCredentialStore { - /// /// Gets the key to be used for signing from the key store, will be the newest key /// /// a key to be used for signing public async Task GetSigningCredentialsAsync() { + using var trace = telemetry.Trace(TelemetryConstants.TraceCategories.Stores, this); + return GetKeys() .Select(x => x.Credentials) .FirstOrDefault(); diff --git a/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerValidationKeysStore.cs b/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerValidationKeysStore.cs index 0be098739..25252d37e 100644 --- a/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerValidationKeysStore.cs +++ b/src/Open.IdentityServer/src/Stores/Compatibility/IdentityServerValidationKeysStore.cs @@ -8,6 +8,7 @@ using Open.IdentityServer.Configuration; using Open.IdentityServer.DataProtection; using Open.IdentityServer.Models; +using Open.IdentityServer.Services; namespace Open.IdentityServer.Stores; @@ -18,11 +19,13 @@ namespace Open.IdentityServer.Stores; /// Converter used to decrypt and convert data-protected key material into usable credentials. /// Time provider implementation /// Compatibility key store options +/// The telemetry service public class IdentityServerValidationKeysStore( IIdentityServerKeyStore identityServerKeyStore, DataProtectedIdentityServerKeyMaterialConverter dataProtectedIdentityServerKeyMaterialConverter, TimeProvider timeProvider, - CompatibilityKeyStoreOptions options + CompatibilityKeyStoreOptions options, + ITelemetryService telemetry ): IdentityServerSigningKeyStore(identityServerKeyStore, dataProtectedIdentityServerKeyMaterialConverter, timeProvider, options), IValidationKeysStore @@ -34,6 +37,8 @@ CompatibilityKeyStoreOptions options /// list of validation keys info public async Task> GetValidationKeysAsync() { + using var trace = telemetry.Trace(TelemetryConstants.TraceCategories.Stores, this); + return GetKeys() .Select(x => new SecurityKeyInfo { Key = x.Credentials.Key, SigningAlgorithm = x.Credentials.Algorithm }) .ToList(); diff --git a/src/Open.IdentityServer/src/Stores/Default/DefaultAuthorizationCodeStore.cs b/src/Open.IdentityServer/src/Stores/Default/DefaultAuthorizationCodeStore.cs index 95f806e94..1d11559e1 100644 --- a/src/Open.IdentityServer/src/Stores/Default/DefaultAuthorizationCodeStore.cs +++ b/src/Open.IdentityServer/src/Stores/Default/DefaultAuthorizationCodeStore.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -22,13 +23,15 @@ public class DefaultAuthorizationCodeStore : DefaultGrantStoreThe store. /// The serializer. /// The handle generation service. + /// The telemetry service /// The logger. public DefaultAuthorizationCodeStore( IPersistedGrantStore store, IPersistentGrantSerializer serializer, IHandleGenerationService handleGenerationService, + ITelemetryService telemetry, ILogger logger) - : base(IdentityServerConstants.PersistedGrantTypes.AuthorizationCode, store, serializer, handleGenerationService, logger) + : base(IdentityServerConstants.PersistedGrantTypes.AuthorizationCode, store, serializer, handleGenerationService, telemetry, logger) { } @@ -39,6 +42,8 @@ public DefaultAuthorizationCodeStore( /// A task that completes when the consent record has been stored. public Task StoreAuthorizationCodeAsync(AuthorizationCode code) { + using var trace = TelemetryService.Trace(TelemetryConstants.TraceCategories.Stores, this); + return CreateItemAsync(code, code.ClientId, code.Subject.GetSubjectId(), code.SessionId, code.Description, code.CreationTime, code.Lifetime); } @@ -49,6 +54,8 @@ public Task StoreAuthorizationCodeAsync(AuthorizationCode code) /// A task that resolves to the record for the specified subject and client, or if not found. public Task GetAuthorizationCodeAsync(string code) { + using var trace = TelemetryService.Trace(TelemetryConstants.TraceCategories.Stores, this); + return GetItemAsync(code); } @@ -58,6 +65,8 @@ public Task GetAuthorizationCodeAsync(string code) /// The code. public Task RemoveAuthorizationCodeAsync(string code) { + using var trace = TelemetryService.Trace(TelemetryConstants.TraceCategories.Stores, this); + return RemoveItemAsync(code); } } \ No newline at end of file diff --git a/src/Open.IdentityServer/src/Stores/Default/DefaultGrantStore.cs b/src/Open.IdentityServer/src/Stores/Default/DefaultGrantStore.cs index 2628c2618..f1275f926 100644 --- a/src/Open.IdentityServer/src/Stores/Default/DefaultGrantStore.cs +++ b/src/Open.IdentityServer/src/Stores/Default/DefaultGrantStore.cs @@ -34,7 +34,7 @@ public class DefaultGrantStore protected IPersistedGrantStore Store { get; } /// - /// The PersistentGrantSerializer; + /// The PersistentGrantSerializer. /// protected IPersistentGrantSerializer Serializer { get; } @@ -42,6 +42,12 @@ public class DefaultGrantStore /// The HandleGenerationService. /// protected IHandleGenerationService HandleGenerationService { get; } + + + /// + /// The TelemetryService. + /// + protected ITelemetryService TelemetryService { get; } /// /// Initializes a new instance of the class. @@ -50,12 +56,14 @@ public class DefaultGrantStore /// The store. /// The serializer. /// The handle generation service. + /// The telemetry service. /// The logger. /// grantType protected DefaultGrantStore(string grantType, IPersistedGrantStore store, IPersistentGrantSerializer serializer, IHandleGenerationService handleGenerationService, + ITelemetryService telemetry, ILogger logger) { if (grantType.IsMissing()) throw new ArgumentNullException(nameof(grantType)); @@ -64,6 +72,7 @@ protected DefaultGrantStore(string grantType, Store = store; Serializer = serializer; HandleGenerationService = handleGenerationService; + TelemetryService = telemetry; Logger = logger; } diff --git a/src/Open.IdentityServer/src/Stores/Default/DefaultReferenceTokenStore.cs b/src/Open.IdentityServer/src/Stores/Default/DefaultReferenceTokenStore.cs index bf349ea39..f67570ac7 100644 --- a/src/Open.IdentityServer/src/Stores/Default/DefaultReferenceTokenStore.cs +++ b/src/Open.IdentityServer/src/Stores/Default/DefaultReferenceTokenStore.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -21,13 +22,15 @@ public class DefaultReferenceTokenStore : DefaultGrantStore, IReferenceTo /// The store. /// The serializer. /// The handle generation service. + /// The telemetry service. /// The logger. public DefaultReferenceTokenStore( IPersistedGrantStore store, IPersistentGrantSerializer serializer, IHandleGenerationService handleGenerationService, + ITelemetryService telemetry, ILogger logger) - : base(IdentityServerConstants.PersistedGrantTypes.ReferenceToken, store, serializer, handleGenerationService, logger) + : base(IdentityServerConstants.PersistedGrantTypes.ReferenceToken, store, serializer, handleGenerationService, telemetry, logger) { } @@ -38,6 +41,8 @@ public DefaultReferenceTokenStore( /// A task that resolves to the handle assigned to the stored reference token. public Task StoreReferenceTokenAsync(Token token) { + using var trace = TelemetryService.Trace(TelemetryConstants.TraceCategories.Stores, this); + return CreateItemAsync(token, token.ClientId, token.SubjectId, token.SessionId, token.Description, token.CreationTime, token.Lifetime); } @@ -48,6 +53,8 @@ public Task StoreReferenceTokenAsync(Token token) /// A task that resolves to the for the given handle, or if not found. public Task GetReferenceTokenAsync(string handle) { + using var trace = TelemetryService.Trace(TelemetryConstants.TraceCategories.Stores, this); + return GetItemAsync(handle); } @@ -57,6 +64,8 @@ public Task GetReferenceTokenAsync(string handle) /// The handle. public Task RemoveReferenceTokenAsync(string handle) { + using var trace = TelemetryService.Trace(TelemetryConstants.TraceCategories.Stores, this); + return RemoveItemAsync(handle); } @@ -67,6 +76,8 @@ public Task RemoveReferenceTokenAsync(string handle) /// The client identifier. public Task RemoveReferenceTokensAsync(string subjectId, string clientId) { + using var trace = TelemetryService.Trace(TelemetryConstants.TraceCategories.Stores, this); + return RemoveAllAsync(subjectId, clientId); } } \ No newline at end of file diff --git a/src/Open.IdentityServer/src/Stores/Default/DefaultRefreshTokenStore.cs b/src/Open.IdentityServer/src/Stores/Default/DefaultRefreshTokenStore.cs index f273d3b7d..9f504367b 100644 --- a/src/Open.IdentityServer/src/Stores/Default/DefaultRefreshTokenStore.cs +++ b/src/Open.IdentityServer/src/Stores/Default/DefaultRefreshTokenStore.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -21,13 +22,15 @@ public class DefaultRefreshTokenStore : DefaultGrantStore, IRefres /// The store. /// The serializer. /// The handle generation service. + /// /// The logger. public DefaultRefreshTokenStore( IPersistedGrantStore store, IPersistentGrantSerializer serializer, IHandleGenerationService handleGenerationService, + ITelemetryService telemetry, ILogger logger) - : base(IdentityServerConstants.PersistedGrantTypes.RefreshToken, store, serializer, handleGenerationService, logger) + : base(IdentityServerConstants.PersistedGrantTypes.RefreshToken, store, serializer, handleGenerationService, telemetry, logger) { } @@ -38,6 +41,8 @@ public DefaultRefreshTokenStore( /// A task that resolves to the handle assigned to the stored refresh token. public async Task StoreRefreshTokenAsync(RefreshToken refreshToken) { + using var trace = TelemetryService.Trace(TelemetryConstants.TraceCategories.Stores, this); + return await CreateItemAsync(refreshToken, refreshToken.ClientId, refreshToken.SubjectId, refreshToken.SessionId, refreshToken.Description, refreshToken.CreationTime, refreshToken.Lifetime); } @@ -48,6 +53,8 @@ public async Task StoreRefreshTokenAsync(RefreshToken refreshToken) /// The refresh token. public Task UpdateRefreshTokenAsync(string handle, RefreshToken refreshToken) { + using var trace = TelemetryService.Trace(TelemetryConstants.TraceCategories.Stores, this); + return StoreItemAsync(handle, refreshToken, refreshToken.ClientId, refreshToken.SubjectId, refreshToken.SessionId, refreshToken.Description, refreshToken.CreationTime, refreshToken.CreationTime.AddSeconds(refreshToken.Lifetime), refreshToken.ConsumedTime); } @@ -58,6 +65,8 @@ public Task UpdateRefreshTokenAsync(string handle, RefreshToken refreshToken) /// A task that resolves to the for the given handle, or if not found. public Task GetRefreshTokenAsync(string refreshTokenHandle) { + using var trace = TelemetryService.Trace(TelemetryConstants.TraceCategories.Stores, this); + return GetItemAsync(refreshTokenHandle); } @@ -67,6 +76,8 @@ public Task GetRefreshTokenAsync(string refreshTokenHandle) /// The refresh token handle. public Task RemoveRefreshTokenAsync(string refreshTokenHandle) { + using var trace = TelemetryService.Trace(TelemetryConstants.TraceCategories.Stores, this); + return RemoveItemAsync(refreshTokenHandle); } @@ -77,6 +88,10 @@ public Task RemoveRefreshTokenAsync(string refreshTokenHandle) /// The client identifier. public Task RemoveRefreshTokensAsync(string subjectId, string clientId) { + using var trace = TelemetryService.Trace(TelemetryConstants.TraceCategories.Stores, this); + trace?.AddTag(TelemetryConstants.TagConstants.Subject, subjectId); + trace?.AddTag(TelemetryConstants.TagConstants.Client, clientId); + return RemoveAllAsync(subjectId, clientId); } } \ No newline at end of file diff --git a/src/Open.IdentityServer/src/Stores/Default/DefaultUserConsentStore.cs b/src/Open.IdentityServer/src/Stores/Default/DefaultUserConsentStore.cs index 3a6d0ba43..1d70d2459 100644 --- a/src/Open.IdentityServer/src/Stores/Default/DefaultUserConsentStore.cs +++ b/src/Open.IdentityServer/src/Stores/Default/DefaultUserConsentStore.cs @@ -22,13 +22,15 @@ public class DefaultUserConsentStore : DefaultGrantStore, IUserConsentS /// The store. /// The serializer. /// The handle generation service. + /// The telemetry service /// The logger. public DefaultUserConsentStore( IPersistedGrantStore store, IPersistentGrantSerializer serializer, IHandleGenerationService handleGenerationService, + ITelemetryService telemetry, ILogger logger) - : base(IdentityServerConstants.PersistedGrantTypes.UserConsent, store, serializer, handleGenerationService, logger) + : base(IdentityServerConstants.PersistedGrantTypes.UserConsent, store, serializer, handleGenerationService, telemetry, logger) { } @@ -44,6 +46,8 @@ private static string GetLegacyConsentKey(string subjectId, string clientId) => /// The consent. public Task StoreUserConsentAsync(Consent consent) { + using var trace = TelemetryService.Trace(TelemetryConstants.TraceCategories.Stores, this); + var key = GetConsentKey(consent.SubjectId, consent.ClientId); return StoreItemAsync(key, consent, consent.ClientId, consent.SubjectId, null, null, consent.CreationTime, consent.Expiration); } @@ -56,6 +60,10 @@ public Task StoreUserConsentAsync(Consent consent) /// A task that resolves to the record for the specified subject and client, or if not found. public async Task GetUserConsentAsync(string subjectId, string clientId) { + using var trace = TelemetryService.Trace(TelemetryConstants.TraceCategories.Stores, this); + trace?.AddTag(TelemetryConstants.TagConstants.Subject, subjectId); + trace?.AddTag(TelemetryConstants.TagConstants.Client, clientId); + var key = GetConsentKey(subjectId, clientId); var item = await GetItemAsync(key); @@ -77,6 +85,10 @@ public async Task GetUserConsentAsync(string subjectId, string clientId /// The client identifier./// A task that resolves to the record for the specified subject and client, or if not found. public Task RemoveUserConsentAsync(string subjectId, string clientId) { + using var trace = TelemetryService.Trace(TelemetryConstants.TraceCategories.Stores, this); + trace?.AddTag(TelemetryConstants.TagConstants.Subject, subjectId); + trace?.AddTag(TelemetryConstants.TagConstants.Client, clientId); + var key = GetConsentKey(subjectId, clientId); return RemoveItemAsync(key); } diff --git a/src/Open.IdentityServer/src/Stores/ValidatingClientStore.cs b/src/Open.IdentityServer/src/Stores/ValidatingClientStore.cs index 54ad5431d..02886c7d8 100644 --- a/src/Open.IdentityServer/src/Stores/ValidatingClientStore.cs +++ b/src/Open.IdentityServer/src/Stores/ValidatingClientStore.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -20,6 +21,7 @@ public class ValidatingClientStore : IClientStore private readonly IClientStore _inner; private readonly IClientConfigurationValidator _validator; private readonly IEventService _events; + private readonly ITelemetryService _telemetry; private readonly ILogger> _logger; private readonly string _validatorType; @@ -29,13 +31,16 @@ public class ValidatingClientStore : IClientStore /// The inner. /// The validator. /// The events. + /// The telemetry /// The logger. - public ValidatingClientStore(T inner, IClientConfigurationValidator validator, IEventService events, ILogger> logger) + public ValidatingClientStore(T inner, IClientConfigurationValidator validator, IEventService events, + ITelemetryService telemetry, ILogger> logger) { _inner = inner; _validator = validator; _events = events; _logger = logger; + _telemetry = telemetry; _validatorType = validator.GetType().FullName; } @@ -49,6 +54,9 @@ public ValidatingClientStore(T inner, IClientConfigurationValidator validator, I /// public async Task FindClientByIdAsync(string clientId) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Stores, this); + trace?.AddTag(TelemetryConstants.TagConstants.Client, clientId); + var client = await _inner.FindClientByIdAsync(clientId); if (client != null) @@ -60,14 +68,14 @@ public async Task FindClientByIdAsync(string clientId) if (context.IsValid) { + _telemetry.CountClientConfigValidation(clientId); _logger.LogDebug("client configuration validation for client {clientId} succeeded.", client.ClientId); return client; } + _telemetry.CountClientConfigValidation(clientId, context.ErrorMessage); _logger.LogError("Invalid client configuration for client {clientId}: {errorMessage}", client.ClientId, context.ErrorMessage); await _events.RaiseAsync(new InvalidClientConfigurationEvent(client, context.ErrorMessage)); - - return null; } return null; diff --git a/src/Open.IdentityServer/src/Validation/Default/ApiSecretValidator.cs b/src/Open.IdentityServer/src/Validation/Default/ApiSecretValidator.cs index 4134fa3d0..1c944aaca 100644 --- a/src/Open.IdentityServer/src/Validation/Default/ApiSecretValidator.cs +++ b/src/Open.IdentityServer/src/Validation/Default/ApiSecretValidator.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -20,6 +21,7 @@ public class ApiSecretValidator : IApiSecretValidator private readonly ILogger _logger; private readonly IResourceStore _resources; private readonly IEventService _events; + private readonly ITelemetryService _telemetry; private readonly ISecretsListParser _parser; private readonly ISecretsListValidator _validator; @@ -30,13 +32,21 @@ public class ApiSecretValidator : IApiSecretValidator /// The parsers. /// The validator. /// The events. + /// The telemetry service /// The logger. - public ApiSecretValidator(IResourceStore resources, ISecretsListParser parsers, ISecretsListValidator validator, IEventService events, ILogger logger) + public ApiSecretValidator( + IResourceStore resources, + ISecretsListParser parsers, + ISecretsListValidator validator, + IEventService events, + ITelemetryService telemetry, + ILogger logger) { _resources = resources; _parser = parsers; _validator = validator; _events = events; + _telemetry = telemetry; _logger = logger; } @@ -49,6 +59,7 @@ public ApiSecretValidator(IResourceStore resources, ISecretsListParser parsers, /// public async Task ValidateAsync(HttpContext context) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Validation, this); _logger.LogTrace("Start API validation"); var fail = new ApiSecretValidationResult @@ -59,7 +70,7 @@ public async Task ValidateAsync(HttpContext context) var parsedSecret = await _parser.ParseAsync(context); if (parsedSecret == null) { - await RaiseFailureEventAsync("unknown", "No API id or secret found"); + await OnFailureAsync("unknown", "No API id or secret found"); _logger.LogError("No API secret found"); return fail; @@ -69,7 +80,7 @@ public async Task ValidateAsync(HttpContext context) var apis = await _resources.FindApiResourcesByNameAsync(new[] { parsedSecret.Id }); if (apis == null || !apis.Any()) { - await RaiseFailureEventAsync(parsedSecret.Id, "Unknown API resource"); + await OnFailureAsync(parsedSecret.Id, "Unknown API resource"); _logger.LogError("No API resource with that name found. aborting"); return fail; @@ -77,7 +88,7 @@ public async Task ValidateAsync(HttpContext context) if (apis.Count() > 1) { - await RaiseFailureEventAsync(parsedSecret.Id, "Invalid API resource"); + await OnFailureAsync(parsedSecret.Id, "Invalid API resource"); _logger.LogError("More than one API resource with that name found. aborting"); return fail; @@ -87,7 +98,7 @@ public async Task ValidateAsync(HttpContext context) if (api.Enabled == false) { - await RaiseFailureEventAsync(parsedSecret.Id, "API resource not enabled"); + await OnFailureAsync(parsedSecret.Id, "API resource not enabled"); _logger.LogError("API resource not enabled. aborting."); return fail; @@ -104,23 +115,25 @@ public async Task ValidateAsync(HttpContext context) Resource = api }; - await RaiseSuccessEventAsync(api.Name, parsedSecret.Type); + await OnSuccessEventAsync(api.Name, parsedSecret.Type); return success; } - await RaiseFailureEventAsync(api.Name, "Invalid API secret"); + await OnFailureAsync(api.Name, "Invalid API secret"); _logger.LogError("API validation failed."); return fail; } - private Task RaiseSuccessEventAsync(string clientId, string authMethod) + private Task OnSuccessEventAsync(string clientId, string authMethod) { + _telemetry.CountApiSecretValidation(client: clientId, authMethod: authMethod); return _events.RaiseAsync(new ApiAuthenticationSuccessEvent(clientId, authMethod)); } - private Task RaiseFailureEventAsync(string clientId, string message) + private Task OnFailureAsync(string clientId, string message) { + _telemetry.CountApiSecretValidation(client: clientId, error: message); return _events.RaiseAsync(new ApiAuthenticationFailureEvent(clientId, message)); } } \ No newline at end of file diff --git a/src/Open.IdentityServer/src/Validation/Default/AuthorizeRequestValidator.cs b/src/Open.IdentityServer/src/Validation/Default/AuthorizeRequestValidator.cs index 28284a1be..b098abe5a 100644 --- a/src/Open.IdentityServer/src/Validation/Default/AuthorizeRequestValidator.cs +++ b/src/Open.IdentityServer/src/Validation/Default/AuthorizeRequestValidator.cs @@ -29,6 +29,7 @@ internal class AuthorizeRequestValidator : IAuthorizeRequestValidator private readonly IUserSession _userSession; private readonly JwtRequestValidator _jwtRequestValidator; private readonly IJwtRequestUriHttpClient _jwtRequestUriHttpClient; + private readonly ITelemetryService _telemetry; private readonly ILogger _logger; private readonly ResponseTypeEqualityComparer @@ -43,6 +44,7 @@ public AuthorizeRequestValidator( IUserSession userSession, JwtRequestValidator jwtRequestValidator, IJwtRequestUriHttpClient jwtRequestUriHttpClient, + ITelemetryService telemetry, ILogger logger) { _options = options; @@ -53,12 +55,14 @@ public AuthorizeRequestValidator( _jwtRequestValidator = jwtRequestValidator; _userSession = userSession; _jwtRequestUriHttpClient = jwtRequestUriHttpClient; + _telemetry = telemetry; _logger = logger; } public async Task ValidateAsync(NameValueCollection parameters, ClaimsPrincipal subject = null) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Validation, this); _logger.LogDebug("Start authorize request protocol validation"); var request = new ValidatedAuthorizeRequest diff --git a/src/Open.IdentityServer/src/Validation/Default/ClientSecretValidator.cs b/src/Open.IdentityServer/src/Validation/Default/ClientSecretValidator.cs index dcbec5cae..b62073a73 100644 --- a/src/Open.IdentityServer/src/Validation/Default/ClientSecretValidator.cs +++ b/src/Open.IdentityServer/src/Validation/Default/ClientSecretValidator.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -20,6 +21,7 @@ public class ClientSecretValidator : IClientSecretValidator private readonly ILogger _logger; private readonly IClientStore _clients; private readonly IEventService _events; + private readonly ITelemetryService _telemetry; private readonly ISecretsListValidator _validator; private readonly ISecretsListParser _parser; @@ -30,14 +32,22 @@ public class ClientSecretValidator : IClientSecretValidator /// The parser. /// The validator. /// The events. + /// The telemetry service. /// The logger. - public ClientSecretValidator(IClientStore clients, ISecretsListParser parser, ISecretsListValidator validator, IEventService events, ILogger logger) + public ClientSecretValidator( + IClientStore clients, + ISecretsListParser parser, + ISecretsListValidator validator, + IEventService events, + ITelemetryService telemetry, + ILogger logger) { _clients = clients; _parser = parser; _validator = validator; _events = events; _logger = logger; + _telemetry = telemetry; } /// @@ -49,6 +59,8 @@ public ClientSecretValidator(IClientStore clients, ISecretsListParser parser, IS /// public async Task ValidateAsync(HttpContext context) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Validation, this); + _logger.LogDebug("Start client validation"); var fail = new ClientSecretValidationResult @@ -59,7 +71,7 @@ public async Task ValidateAsync(HttpContext contex var parsedSecret = await _parser.ParseAsync(context); if (parsedSecret == null) { - await RaiseFailureEventAsync("unknown", "No client id found"); + await OnFailureAsync("unknown", "No client id found"); _logger.LogError("No client identifier found"); return fail; @@ -69,7 +81,7 @@ public async Task ValidateAsync(HttpContext contex var client = await _clients.FindEnabledClientByIdAsync(parsedSecret.Id); if (client == null) { - await RaiseFailureEventAsync(parsedSecret.Id, "Unknown client"); + await OnFailureAsync(parsedSecret.Id, "Unknown client"); _logger.LogError("No client with id '{clientId}' found. aborting", parsedSecret.Id); return fail; @@ -85,7 +97,7 @@ public async Task ValidateAsync(HttpContext contex secretValidationResult = await _validator.ValidateAsync(client.ClientSecrets, parsedSecret); if (secretValidationResult.Success == false) { - await RaiseFailureEventAsync(client.ClientId, "Invalid client secret"); + await OnFailureAsync(client.ClientId, "Invalid client secret"); _logger.LogError("Client secret validation failed for client: {clientId}.", client.ClientId); return fail; @@ -102,17 +114,19 @@ public async Task ValidateAsync(HttpContext contex Confirmation = secretValidationResult?.Confirmation }; - await RaiseSuccessEventAsync(client.ClientId, parsedSecret.Type); + await OnSuccessAsync(client.ClientId, parsedSecret.Type); return success; } - private Task RaiseSuccessEventAsync(string clientId, string authMethod) + private Task OnSuccessAsync(string clientId, string authMethod) { + _telemetry.CountClientSecretValidation(clientId, authMethod); return _events.RaiseAsync(new ClientAuthenticationSuccessEvent(clientId, authMethod)); } - private Task RaiseFailureEventAsync(string clientId, string message) + private Task OnFailureAsync(string clientId, string message) { + _telemetry.CountClientSecretValidation(clientId, error: message); return _events.RaiseAsync(new ClientAuthenticationFailureEvent(clientId, message)); } } \ No newline at end of file diff --git a/src/Open.IdentityServer/src/Validation/Default/DefaultClientConfigurationValidator.cs b/src/Open.IdentityServer/src/Validation/Default/DefaultClientConfigurationValidator.cs index 5178a81d4..30bc403ad 100644 --- a/src/Open.IdentityServer/src/Validation/Default/DefaultClientConfigurationValidator.cs +++ b/src/Open.IdentityServer/src/Validation/Default/DefaultClientConfigurationValidator.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. using Open.IdentityServer.Configuration; @@ -6,6 +7,7 @@ using System; using System.Linq; using System.Threading.Tasks; +using Open.IdentityServer.Services; namespace Open.IdentityServer.Validation; @@ -16,14 +18,17 @@ namespace Open.IdentityServer.Validation; public class DefaultClientConfigurationValidator : IClientConfigurationValidator { private readonly IdentityServerOptions _options; + private readonly ITelemetryService _telemetry; /// /// Initializes a new instance of the class. /// /// The IdentityServer options used to determine validation rules such as invalid redirect URI schemes. - public DefaultClientConfigurationValidator(IdentityServerOptions options) + /// The telemetry service + public DefaultClientConfigurationValidator(IdentityServerOptions options, ITelemetryService telemetry) { _options = options; + _telemetry = telemetry; } /// @@ -32,6 +37,8 @@ public DefaultClientConfigurationValidator(IdentityServerOptions options) /// The context. public async Task ValidateAsync(ClientConfigurationValidationContext context) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Validation, this); + if (context.Client.ProtocolType == IdentityServerConstants.ProtocolTypes.OpenIdConnect) { await ValidateGrantTypesAsync(context); diff --git a/src/Open.IdentityServer/src/Validation/Default/DefaultResourceValidator.cs b/src/Open.IdentityServer/src/Validation/Default/DefaultResourceValidator.cs index c8b045322..18323d83c 100644 --- a/src/Open.IdentityServer/src/Validation/Default/DefaultResourceValidator.cs +++ b/src/Open.IdentityServer/src/Validation/Default/DefaultResourceValidator.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. using Open.IdentityServer.Models; @@ -8,6 +9,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Open.IdentityServer.Services; namespace Open.IdentityServer.Validation; @@ -18,6 +20,7 @@ public class DefaultResourceValidator : IResourceValidator { private readonly ILogger _logger; private readonly IScopeParser _scopeParser; + private readonly ITelemetryService _telemetry; private readonly IResourceStore _store; /// @@ -25,12 +28,17 @@ public class DefaultResourceValidator : IResourceValidator /// /// The store. /// The scope parser used to parse raw scope strings into structured scope values. + /// The telemetry service /// The logger. - public DefaultResourceValidator(IResourceStore store, IScopeParser scopeParser, + public DefaultResourceValidator( + IResourceStore store, + IScopeParser scopeParser, + ITelemetryService telemetry, ILogger logger) { _logger = logger; _scopeParser = scopeParser; + _telemetry = telemetry; _store = store; } @@ -39,6 +47,7 @@ public virtual async Task ValidateRequestedResourcesAs ResourceValidationRequest request) { if (request == null) throw new ArgumentNullException(nameof(request)); + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Validation, this); var parsedScopesResult = _scopeParser.ParseScopeValues(request.Scopes); diff --git a/src/Open.IdentityServer/src/Validation/Default/DeviceAuthorizationRequestValidator.cs b/src/Open.IdentityServer/src/Validation/Default/DeviceAuthorizationRequestValidator.cs index 2e61e7799..6084eb6d8 100644 --- a/src/Open.IdentityServer/src/Validation/Default/DeviceAuthorizationRequestValidator.cs +++ b/src/Open.IdentityServer/src/Validation/Default/DeviceAuthorizationRequestValidator.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -12,6 +13,7 @@ using Open.IdentityServer.Logging; using Open.IdentityServer.Models; using Microsoft.Extensions.Logging; +using Open.IdentityServer.Services; namespace Open.IdentityServer.Validation; @@ -19,21 +21,25 @@ internal class DeviceAuthorizationRequestValidator : IDeviceAuthorizationRequest { private readonly IdentityServerOptions _options; private readonly IResourceValidator _resourceValidator; + private readonly ITelemetryService _telemetry; private readonly ILogger _logger; public DeviceAuthorizationRequestValidator( IdentityServerOptions options, IResourceValidator resourceValidator, + ITelemetryService telemetry, ILogger logger) { _options = options; _resourceValidator = resourceValidator; + _telemetry = telemetry; _logger = logger; } /// public async Task ValidateAsync(NameValueCollection parameters, ClientSecretValidationResult clientValidationResult) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Validation, this); _logger.LogDebug("Start device authorization request validation"); var request = new ValidatedDeviceAuthorizationRequest diff --git a/src/Open.IdentityServer/src/Validation/Default/DeviceCodeValidator.cs b/src/Open.IdentityServer/src/Validation/Default/DeviceCodeValidator.cs index 84c268356..7a5a4d57c 100644 --- a/src/Open.IdentityServer/src/Validation/Default/DeviceCodeValidator.cs +++ b/src/Open.IdentityServer/src/Validation/Default/DeviceCodeValidator.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -21,6 +22,7 @@ internal class DeviceCodeValidator : IDeviceCodeValidator private readonly IProfileService _profile; private readonly IDeviceFlowThrottlingService _throttlingService; private readonly TimeProvider _systemClock; + private readonly ITelemetryService _telemetry; private readonly ILogger _logger; /// @@ -30,18 +32,21 @@ internal class DeviceCodeValidator : IDeviceCodeValidator /// The profile. /// The throttling service. /// The system clock. + /// The telemetry service /// The logger. public DeviceCodeValidator( IDeviceFlowCodeService devices, IProfileService profile, IDeviceFlowThrottlingService throttlingService, TimeProvider systemClock, + ITelemetryService telemetry, ILogger logger) { _devices = devices; _profile = profile; _throttlingService = throttlingService; _systemClock = systemClock; + _telemetry = telemetry; _logger = logger; } @@ -52,6 +57,8 @@ public DeviceCodeValidator( /// public async Task ValidateAsync(DeviceCodeValidationContext context) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Validation, this); + var deviceCode = await _devices.FindByDeviceCodeAsync(context.DeviceCode); if (deviceCode == null) diff --git a/src/Open.IdentityServer/src/Validation/Default/EndSessionRequestValidator.cs b/src/Open.IdentityServer/src/Validation/Default/EndSessionRequestValidator.cs index 2d30222ed..d22db434f 100644 --- a/src/Open.IdentityServer/src/Validation/Default/EndSessionRequestValidator.cs +++ b/src/Open.IdentityServer/src/Validation/Default/EndSessionRequestValidator.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -48,6 +49,11 @@ public class EndSessionRequestValidator : IEndSessionRequestValidator /// protected readonly IUserSession UserSession; + /// + /// The telemetry service. + /// + protected readonly ITelemetryService Telemetry; + /// /// The logout notification service. /// @@ -73,6 +79,7 @@ public class EndSessionRequestValidator : IEndSessionRequestValidator /// The user session service used to retrieve the current session and client list. /// The service used to build front-channel logout notification URLs. /// The message store used to read end-session callback messages. + /// The telemetry service. /// The logger. public EndSessionRequestValidator( IHttpContextAccessor context, @@ -82,6 +89,7 @@ public EndSessionRequestValidator( IUserSession userSession, ILogoutNotificationService logoutNotificationService, IMessageStore endSessionMessageStore, + ITelemetryService telemetry, ILogger logger) { Context = context; @@ -91,12 +99,14 @@ public EndSessionRequestValidator( UserSession = userSession; LogoutNotificationService = logoutNotificationService; EndSessionMessageStore = endSessionMessageStore; + Telemetry = telemetry; Logger = logger; } /// public async Task ValidateAsync(NameValueCollection parameters, ClaimsPrincipal subject) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Validation, this); Logger.LogDebug("Start end session request validation"); var isAuthenticated = subject.IsAuthenticated(); @@ -216,6 +226,7 @@ protected virtual void LogSuccess(ValidatedEndSessionRequest request) /// public async Task ValidateCallbackAsync(NameValueCollection parameters) { + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Validation, this); var result = new EndSessionCallbackValidationResult { IsError = true diff --git a/src/Open.IdentityServer/src/Validation/Default/JwtRequestValidator.cs b/src/Open.IdentityServer/src/Validation/Default/JwtRequestValidator.cs index 1f858c87a..ff0792a45 100644 --- a/src/Open.IdentityServer/src/Validation/Default/JwtRequestValidator.cs +++ b/src/Open.IdentityServer/src/Validation/Default/JwtRequestValidator.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -15,6 +16,7 @@ using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; +using Open.IdentityServer.Services; namespace Open.IdentityServer.Validation; @@ -59,18 +61,29 @@ protected string AudienceUri /// The IdentityServer options. /// protected readonly IdentityServerOptions Options; + + /// + /// The Telemetry service. + /// + protected readonly ITelemetryService Telemetry; /// /// Initializes a new instance of the class. /// /// The HTTP context accessor used to determine the audience URI. /// The IdentityServer options. + /// The telemetry service. /// The logger. - public JwtRequestValidator(IHttpContextAccessor contextAccessor, IdentityServerOptions options, ILogger logger) + public JwtRequestValidator( + IHttpContextAccessor contextAccessor, + IdentityServerOptions options, + ITelemetryService telemetry, + ILogger logger) { _httpContextAccessor = contextAccessor; Options = options; + Telemetry = telemetry; Logger = logger; } @@ -97,6 +110,8 @@ public virtual async Task ValidateAsync(Client clien if (client == null) throw new ArgumentNullException(nameof(client)); if (String.IsNullOrWhiteSpace(jwtTokenString)) throw new ArgumentNullException(nameof(jwtTokenString)); + using var trace = Telemetry.Trace(TelemetryConstants.TraceCategories.Validation, this); + var fail = new JwtRequestValidationResult { IsError = true }; List trustedKeys; diff --git a/src/Open.IdentityServer/src/Validation/Default/TokenRequestValidator.cs b/src/Open.IdentityServer/src/Validation/Default/TokenRequestValidator.cs index 9d7b0183a..fde175d8e 100644 --- a/src/Open.IdentityServer/src/Validation/Default/TokenRequestValidator.cs +++ b/src/Open.IdentityServer/src/Validation/Default/TokenRequestValidator.cs @@ -35,6 +35,7 @@ internal class TokenRequestValidator : ITokenRequestValidator private readonly IProfileService _profile; private readonly IDeviceCodeValidator _deviceCodeValidator; private readonly TimeProvider _clock; + private readonly ITelemetryService _telemetry; private readonly ILogger _logger; private ValidatedTokenRequest _validatedRequest; @@ -55,6 +56,7 @@ internal class TokenRequestValidator : ITokenRequestValidator /// The refresh token service used to validate refresh tokens. /// The events. /// The clock. + /// the telemetry service /// The logger. public TokenRequestValidator(IdentityServerOptions options, IAuthorizationCodeStore authorizationCodeStore, @@ -69,6 +71,7 @@ public TokenRequestValidator(IdentityServerOptions options, IRefreshTokenService refreshTokenService, IEventService events, TimeProvider clock, + ITelemetryService telemetry, ILogger logger) { _logger = logger; @@ -85,6 +88,7 @@ public TokenRequestValidator(IdentityServerOptions options, _tokenValidator = tokenValidator; _refreshTokenService = refreshTokenService; _events = events; + _telemetry = telemetry; } /// @@ -101,6 +105,8 @@ public TokenRequestValidator(IdentityServerOptions options, public async Task ValidateRequestAsync(NameValueCollection parameters, ClientSecretValidationResult clientValidationResult) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Validation, this); + _logger.LogDebug("Start token request validation"); _validatedRequest = new ValidatedTokenRequest @@ -961,11 +967,13 @@ private void LogSuccess() private Task RaiseSuccessfulResourceOwnerAuthenticationEventAsync(string userName, string subjectId, string clientId) { + _telemetry.CountResourceOwnerAuthentication(clientId); return _events.RaiseAsync(new UserLoginSuccessEvent(userName, subjectId, null, interactive: false, clientId)); } private Task RaiseFailedResourceOwnerAuthenticationEventAsync(string userName, string error, string clientId) { + _telemetry.CountResourceOwnerAuthentication(clientId, error); return _events.RaiseAsync(new UserLoginFailureEvent(userName, error, interactive: false, clientId: clientId)); } } \ No newline at end of file diff --git a/src/Open.IdentityServer/src/Validation/Default/TokenValidator.cs b/src/Open.IdentityServer/src/Validation/Default/TokenValidator.cs index f3838cc92..0a187b790 100644 --- a/src/Open.IdentityServer/src/Validation/Default/TokenValidator.cs +++ b/src/Open.IdentityServer/src/Validation/Default/TokenValidator.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -33,6 +34,7 @@ internal class TokenValidator : ITokenValidator private readonly IKeyMaterialService _keys; private readonly TimeProvider _clock; private readonly TokenValidationLog _log; + private readonly ITelemetryService _telemetry; public TokenValidator( IdentityServerOptions options, @@ -44,6 +46,7 @@ public TokenValidator( ICustomTokenValidator customValidator, IKeyMaterialService keys, TimeProvider clock, + ITelemetryService telemetry, ILogger logger) { _options = options; @@ -55,12 +58,14 @@ public TokenValidator( _keys = keys; _clock = clock; _logger = logger; + _telemetry = telemetry; _log = new TokenValidationLog(); } public async Task ValidateIdentityTokenAsync(string token, string clientId = null, bool validateLifetime = true) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Validation, this); _logger.LogDebug("Start identity token validation"); if (token.Length > _options.InputLengthRestrictions.Jwt) @@ -121,6 +126,7 @@ public async Task ValidateIdentityTokenAsync(string token public async Task ValidateAccessTokenAsync(string token, string expectedScope = null) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Validation, this); _logger.LogTrace("Start access token validation"); _log.ExpectedScope = expectedScope; diff --git a/src/Open.IdentityServer/src/Validation/Default/UserInfoRequestValidator.cs b/src/Open.IdentityServer/src/Validation/Default/UserInfoRequestValidator.cs index 92de7e5a2..ddcec31e5 100644 --- a/src/Open.IdentityServer/src/Validation/Default/UserInfoRequestValidator.cs +++ b/src/Open.IdentityServer/src/Validation/Default/UserInfoRequestValidator.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -20,6 +21,7 @@ internal class UserInfoRequestValidator : IUserInfoRequestValidator { private readonly ITokenValidator _tokenValidator; private readonly IProfileService _profile; + private readonly ITelemetryService _telemetry; private readonly ILogger _logger; /// @@ -27,14 +29,17 @@ internal class UserInfoRequestValidator : IUserInfoRequestValidator /// /// The token validator. /// The profile service + /// The telemetry service. /// The logger. public UserInfoRequestValidator( ITokenValidator tokenValidator, IProfileService profile, + ITelemetryService telemetry, ILogger logger) { _tokenValidator = tokenValidator; _profile = profile; + _telemetry = telemetry; _logger = logger; } @@ -43,9 +48,10 @@ public UserInfoRequestValidator( /// /// The access token. /// - /// public async Task ValidateRequestAsync(string accessToken) { + using var trace = _telemetry.Trace(TelemetryConstants.TraceCategories.Validation, this); + // the access token needs to be valid and have at least the openid scope var tokenResult = await _tokenValidator.ValidateAccessTokenAsync( accessToken, diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/IAuthenticationSchemeHandler.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/IAuthenticationSchemeHandler.cs index 68c415088..52386cafd 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/IAuthenticationSchemeHandler.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/IAuthenticationSchemeHandler.cs @@ -1,7 +1,7 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -namespace IdentityServer.UnitTests.Common; +namespace Open.IdentityServer.UnitTests.Common; internal interface IAuthenticationSchemeHandler { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockAuthenticationHandler.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockAuthenticationHandler.cs index b0e2e84a7..4354e8f83 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockAuthenticationHandler.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockAuthenticationHandler.cs @@ -5,7 +5,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; -namespace IdentityServer.UnitTests.Common; +namespace Open.IdentityServer.UnitTests.Common; internal class MockAuthenticationHandler : IAuthenticationHandler { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockAuthenticationHandlerProvider.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockAuthenticationHandlerProvider.cs index a469e5ed3..f51b33ccf 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockAuthenticationHandlerProvider.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockAuthenticationHandlerProvider.cs @@ -5,7 +5,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; -namespace IdentityServer.UnitTests.Common; +namespace Open.IdentityServer.UnitTests.Common; internal class MockAuthenticationHandlerProvider : IAuthenticationHandlerProvider { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockAuthenticationSchemeProvider.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockAuthenticationSchemeProvider.cs index 4f9013e91..9d8ec092f 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockAuthenticationSchemeProvider.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockAuthenticationSchemeProvider.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; -namespace IdentityServer.UnitTests.Common; +namespace Open.IdentityServer.UnitTests.Common; internal class MockAuthenticationSchemeProvider : IAuthenticationSchemeProvider { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockAuthenticationService.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockAuthenticationService.cs index ff8a315f0..c18bd9f99 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockAuthenticationService.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockAuthenticationService.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; -namespace IdentityServer.UnitTests.Common; +namespace Open.IdentityServer.UnitTests.Common; internal class MockAuthenticationService : IAuthenticationService { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockClaimsService.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockClaimsService.cs index 58adaaa84..6dad002ae 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockClaimsService.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockClaimsService.cs @@ -8,7 +8,7 @@ using System.Security.Claims; using System.Threading.Tasks; -namespace IdentityServer.UnitTests.Common; +namespace Open.IdentityServer.UnitTests.Common; class MockClaimsService : IClaimsService { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockClientSessionService.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockClientSessionService.cs index 8bfb47936..d15b9ed96 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockClientSessionService.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockClientSessionService.cs @@ -3,7 +3,7 @@ -namespace IdentityServer.UnitTests.Common; +namespace Open.IdentityServer.UnitTests.Common; //class MockClientSessionService : IClientSessionService //{ diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockConsentMessageStore.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockConsentMessageStore.cs index 0fc7bfdf2..3b5523759 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockConsentMessageStore.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockConsentMessageStore.cs @@ -7,7 +7,7 @@ using Open.IdentityServer.Models; using Open.IdentityServer.Stores; -namespace IdentityServer.UnitTests.Common; +namespace Open.IdentityServer.UnitTests.Common; public class MockConsentMessageStore : IConsentMessageStore { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockConsentService.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockConsentService.cs index ae7f97cdc..adfc5657c 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockConsentService.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockConsentService.cs @@ -10,7 +10,7 @@ using Open.IdentityServer.Services; using Open.IdentityServer.Validation; -namespace IdentityServer.UnitTests.Common; +namespace Open.IdentityServer.UnitTests.Common; public class MockConsentService : IConsentService { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockHttpContextAccessor.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockHttpContextAccessor.cs index 168ca692a..99472561e 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockHttpContextAccessor.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockHttpContextAccessor.cs @@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -namespace IdentityServer.UnitTests.Common; +namespace Open.IdentityServer.UnitTests.Common; internal class MockHttpContextAccessor : IHttpContextAccessor { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockKeyMaterialService.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockKeyMaterialService.cs index 02edc20ee..b74a3c1fd 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockKeyMaterialService.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockKeyMaterialService.cs @@ -8,7 +8,7 @@ using System.Linq; using System.Threading.Tasks; -namespace IdentityServer.UnitTests.Common; +namespace Open.IdentityServer.UnitTests.Common; class MockKeyMaterialService : IKeyMaterialService { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockLogoutNotificationService.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockLogoutNotificationService.cs index cff3afeb1..bcb3ac4af 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockLogoutNotificationService.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockLogoutNotificationService.cs @@ -7,7 +7,7 @@ using System.Linq; using System.Threading.Tasks; -namespace IdentityServer.UnitTests.Common; +namespace Open.IdentityServer.UnitTests.Common; public class MockLogoutNotificationService : ILogoutNotificationService { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockMessageStore.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockMessageStore.cs index be1ad0a92..37453b3f7 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockMessageStore.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockMessageStore.cs @@ -8,7 +8,7 @@ using Open.IdentityServer.Models; using Open.IdentityServer.Stores; -namespace IdentityServer.UnitTests.Common; +namespace Open.IdentityServer.UnitTests.Common; public class MockMessageStore : IMessageStore { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockPersistedGrantService.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockPersistedGrantService.cs index a34b61383..5fbdc122d 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockPersistedGrantService.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockPersistedGrantService.cs @@ -8,7 +8,7 @@ using Open.IdentityServer.Models; using Open.IdentityServer.Services; -namespace IdentityServer.UnitTests.Common; +namespace Open.IdentityServer.UnitTests.Common; public class MockPersistedGrantService : IPersistedGrantService { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockProfileService.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockProfileService.cs index e970d4f75..b4c2805a1 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockProfileService.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockProfileService.cs @@ -9,7 +9,7 @@ using Open.IdentityServer.Models; using Open.IdentityServer.Services; -namespace IdentityServer.UnitTests.Common; +namespace Open.IdentityServer.UnitTests.Common; public class MockProfileService : IProfileService { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockReferenceTokenStore.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockReferenceTokenStore.cs index e805c1cbd..c4e9d1d19 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockReferenceTokenStore.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockReferenceTokenStore.cs @@ -6,7 +6,7 @@ using System; using System.Threading.Tasks; -namespace IdentityServer.UnitTests.Common; +namespace Open.IdentityServer.UnitTests.Common; class MockReferenceTokenStore : IReferenceTokenStore { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockResourceValidator.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockResourceValidator.cs index 247c6c35b..a5d61e669 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockResourceValidator.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockResourceValidator.cs @@ -6,7 +6,7 @@ using System.Linq; using System.Threading.Tasks; -namespace IdentityServer.UnitTests.Common; +namespace Open.IdentityServer.UnitTests.Common; class MockResourceValidator : IResourceValidator { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockReturnUrlParser.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockReturnUrlParser.cs index bef2236f3..8ef813822 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockReturnUrlParser.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockReturnUrlParser.cs @@ -1,20 +1,22 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. using System.Linq; using System.Threading.Tasks; +using Moq; using Open.IdentityServer.Models; using Open.IdentityServer.Services; -namespace IdentityServer.UnitTests.Common; +namespace Open.IdentityServer.UnitTests.Common; public class MockReturnUrlParser : ReturnUrlParser { public AuthorizationRequest AuthorizationRequestResult { get; set; } public bool IsValidReturnUrlResult { get; set; } - public MockReturnUrlParser() : base(Enumerable.Empty()) + public MockReturnUrlParser() : base(Enumerable.Empty(), Mock.Of()) { } diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockSessionIdService.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockSessionIdService.cs index d361a6176..371be8559 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockSessionIdService.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockSessionIdService.cs @@ -3,7 +3,7 @@ -namespace IdentityServer.UnitTests.Common; +namespace Open.IdentityServer.UnitTests.Common; //public class MockSessionIdService : ISessionIdService //{ diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockSystemClock.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockSystemClock.cs index ed8f4d820..350d36e96 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockSystemClock.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockSystemClock.cs @@ -3,7 +3,7 @@ using System; -namespace IdentityServer.UnitTests.Common; +namespace Open.IdentityServer.UnitTests.Common; class MockSystemClock : TimeProvider { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockTokenCreationService.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockTokenCreationService.cs index bc13e57a1..cde518756 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockTokenCreationService.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockTokenCreationService.cs @@ -5,7 +5,7 @@ using Open.IdentityServer.Services; using System.Threading.Tasks; -namespace IdentityServer.UnitTests.Common; +namespace Open.IdentityServer.UnitTests.Common; class MockTokenCreationService : ITokenCreationService { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockUserSession.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockUserSession.cs index 448fbd13c..eceee62e9 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockUserSession.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/MockUserSession.cs @@ -9,7 +9,7 @@ using Open.IdentityServer.Services; using Microsoft.AspNetCore.Authentication; -namespace IdentityServer.UnitTests.Common; +namespace Open.IdentityServer.UnitTests.Common; public class MockUserSession : IUserSession { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/NetworkHandler.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/NetworkHandler.cs index a9bdc0cc0..a7bc4cbea 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/NetworkHandler.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/NetworkHandler.cs @@ -7,7 +7,7 @@ using System.Threading; using System.Threading.Tasks; -namespace IdentityServer.UnitTests.Common; +namespace Open.IdentityServer.UnitTests.Common; public class NetworkHandler : HttpMessageHandler { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/StubAuthorizeResponseGenerator.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/StubAuthorizeResponseGenerator.cs index 7bed6e304..6bebe8a85 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/StubAuthorizeResponseGenerator.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/StubAuthorizeResponseGenerator.cs @@ -6,7 +6,7 @@ using Open.IdentityServer.ResponseHandling; using Open.IdentityServer.Validation; -namespace IdentityServer.UnitTests.Common; +namespace Open.IdentityServer.UnitTests.Common; internal class StubAuthorizeResponseGenerator : IAuthorizeResponseGenerator { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/StubClock.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/StubClock.cs index 549d975db..4f2f304c7 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/StubClock.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/StubClock.cs @@ -4,7 +4,7 @@ using System; -namespace IdentityServer.UnitTests.Common; +namespace Open.IdentityServer.UnitTests.Common; internal class StubClock : TimeProvider { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/StubHandleGenerationService.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/StubHandleGenerationService.cs index 9c553db00..fe192dba0 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/StubHandleGenerationService.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/StubHandleGenerationService.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using Open.IdentityServer.Services; -namespace IdentityServer.UnitTests.Common; +namespace Open.IdentityServer.UnitTests.Common; public class StubHandleGenerationService : DefaultHandleGenerationService, IHandleGenerationService { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/TestCert.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/TestCert.cs index 6d66e0853..78bfcdb8a 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/TestCert.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/TestCert.cs @@ -6,7 +6,7 @@ using System.Security.Cryptography.X509Certificates; using Microsoft.IdentityModel.Tokens; -namespace IdentityServer.UnitTests.Common; +namespace Open.IdentityServer.UnitTests.Common; internal static class TestCert { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/TestEventService.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/TestEventService.cs index c449c077d..dedf7efa2 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/TestEventService.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/TestEventService.cs @@ -10,7 +10,7 @@ using Open.IdentityServer.Events; using Open.IdentityServer.Services; -namespace IdentityServer.UnitTests.Common; +namespace Open.IdentityServer.UnitTests.Common; public class TestEventService : IEventService { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/TestExtensions.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/TestExtensions.cs index 0ccc99fde..571bf6b47 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/TestExtensions.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/TestExtensions.cs @@ -4,7 +4,7 @@ using System.Linq; -namespace IdentityServer.UnitTests.Common; +namespace Open.IdentityServer.UnitTests.Common; internal static class TestExtensions { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/TestIdentityServerOptions.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/TestIdentityServerOptions.cs index c8cf41fa5..79abdff24 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/TestIdentityServerOptions.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/TestIdentityServerOptions.cs @@ -4,7 +4,7 @@ using Open.IdentityServer.Configuration; -namespace IdentityServer.UnitTests.Common; +namespace Open.IdentityServer.UnitTests.Common; internal class TestIdentityServerOptions { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/TestLogger.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/TestLogger.cs index e3d14dc0a..cf4f24237 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/TestLogger.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/TestLogger.cs @@ -4,7 +4,7 @@ using Microsoft.Extensions.Logging; -namespace IdentityServer.UnitTests.Common; +namespace Open.IdentityServer.UnitTests.Common; public static class TestLogger { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/TestUserConsentStore.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/TestUserConsentStore.cs index 143fd939f..bf1d480d1 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/TestUserConsentStore.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Common/TestUserConsentStore.cs @@ -7,8 +7,9 @@ using Open.IdentityServer.Services; using Open.IdentityServer.Stores; using Open.IdentityServer.Stores.Serialization; +using Open.IdentityServer.UnitTests.Validation.Setup; -namespace IdentityServer.UnitTests.Common; +namespace Open.IdentityServer.UnitTests.Common; public class TestUserConsentStore : IUserConsentStore { @@ -21,6 +22,7 @@ public TestUserConsentStore() _grantStore, new PersistentGrantSerializer(), new DefaultHandleGenerationService(), + new NopTelemetryService(), TestLogger.Create()); } diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Cors/MockCorsPolicyProvider.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Cors/MockCorsPolicyProvider.cs index c5e4a3960..fd32cfcec 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Cors/MockCorsPolicyProvider.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Cors/MockCorsPolicyProvider.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Cors.Infrastructure; using Microsoft.AspNetCore.Http; -namespace IdentityServer.UnitTests.Cors; +namespace Open.IdentityServer.UnitTests.Cors; public class MockCorsPolicyProvider : ICorsPolicyProvider { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Cors/MockCorsPolicyService.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Cors/MockCorsPolicyService.cs index f40a0b5c8..8282c286d 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Cors/MockCorsPolicyService.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Cors/MockCorsPolicyService.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using Open.IdentityServer.Services; -namespace IdentityServer.UnitTests.Cors; +namespace Open.IdentityServer.UnitTests.Cors; public class MockCorsPolicyService : ICorsPolicyService { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Cors/PolicyProviderTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Cors/PolicyProviderTests.cs index df6369a3e..87f4e1dfb 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Cors/PolicyProviderTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Cors/PolicyProviderTests.cs @@ -6,7 +6,7 @@ using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer.Configuration; using Open.IdentityServer.Configuration.DependencyInjection; using Open.IdentityServer.Hosting; @@ -16,7 +16,7 @@ using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace IdentityServer.UnitTests.Cors; +namespace Open.IdentityServer.UnitTests.Cors; [SuppressMessage( "Usage", diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/DataProtection/DataProtectedGrantDataConverterTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/DataProtection/DataProtectedGrantDataConverterTests.cs index 13e47ebe0..a9d5cf1ca 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/DataProtection/DataProtectedGrantDataConverterTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/DataProtection/DataProtectedGrantDataConverterTests.cs @@ -6,7 +6,7 @@ using Open.IdentityServer.DataProtection; using Xunit; -namespace IdentityServer.UnitTests.DataProtection; +namespace Open.IdentityServer.UnitTests.DataProtection; public class DataProtectedGrantDataConverterTests { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/DataProtection/DataProtectedIdentityServerKeyMaterialConverterTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/DataProtection/DataProtectedIdentityServerKeyMaterialConverterTests.cs index 44c9159b0..ab6980b80 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/DataProtection/DataProtectedIdentityServerKeyMaterialConverterTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/DataProtection/DataProtectedIdentityServerKeyMaterialConverterTests.cs @@ -6,17 +6,16 @@ using System.Security.Cryptography.X509Certificates; using System.Text; using AwesomeAssertions; -using IdentityServer.UnitTests.Stores.Compatibility; +using Open.IdentityServer.UnitTests.Stores.Compatibility; using Microsoft.AspNetCore.DataProtection; using Microsoft.IdentityModel.Tokens; using Moq; -using Open.IdentityServer; using Open.IdentityServer.Configuration; using Open.IdentityServer.DataProtection; using Open.IdentityServer.Models; using Xunit; -namespace IdentityServer.UnitTests.DataProtection; +namespace Open.IdentityServer.UnitTests.DataProtection; public class DataProtectedIdentityServerKeyMaterialConverterTests { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/DataProtection/PersistentGrantSerializerDataProtectionDecoratorTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/DataProtection/PersistentGrantSerializerDataProtectionDecoratorTests.cs index be778e71c..8bac44d7e 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/DataProtection/PersistentGrantSerializerDataProtectionDecoratorTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/DataProtection/PersistentGrantSerializerDataProtectionDecoratorTests.cs @@ -12,7 +12,7 @@ using Moq; using Xunit; -namespace IdentityServer.UnitTests.DataProtection; +namespace Open.IdentityServer.UnitTests.DataProtection; public class FakeData { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Authorize/AuthorizeCallbackEndpointTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Authorize/AuthorizeCallbackEndpointTests.cs index 0f6d92cca..479fb8076 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Authorize/AuthorizeCallbackEndpointTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Authorize/AuthorizeCallbackEndpointTests.cs @@ -5,8 +5,7 @@ using System.Security.Claims; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; -using Open.IdentityServer; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer.Configuration; using Open.IdentityServer.Endpoints; using Open.IdentityServer.Endpoints.Results; @@ -15,9 +14,11 @@ using Open.IdentityServer.Validation; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; +using Moq; +using Open.IdentityServer.Services; using Xunit; -namespace IdentityServer.UnitTests.Endpoints.Authorize; +namespace Open.IdentityServer.UnitTests.Endpoints.Authorize; public class AuthorizeCallbackEndpointTests { @@ -49,6 +50,9 @@ public class AuthorizeCallbackEndpointTests private ValidatedAuthorizeRequest _validatedAuthorizeRequest; + private Mock _telemetry; + private Mock _trace; + public AuthorizeCallbackEndpointTests() { Init(); @@ -203,6 +207,19 @@ public async Task ProcessAsync_valid_consent_message_should_return_authorize_res result.Should().BeOfType(); } + [Fact] + [Trait("Category", Category)] + public async Task process_should_initiate_telemetry_trace() + { + await _subject.ProcessAsync(_context); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Basic, + _subject, + "ProcessAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } + internal void Init() { _context = new MockHttpContextAccessor().HttpContext; @@ -225,6 +242,11 @@ internal void Init() _stubAuthorizeRequestValidator.Result = new AuthorizeRequestValidationResult(_validatedAuthorizeRequest); + _telemetry = new(); + _trace = new(); + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + _subject = new AuthorizeCallbackEndpoint( _fakeEventService, _fakeLogger, @@ -233,6 +255,7 @@ internal void Init() _stubInteractionGenerator, _stubAuthorizeResponseGenerator, _mockUserSession, - _mockUserConsentResponseMessageStore); + _mockUserConsentResponseMessageStore, + _telemetry.Object); } } \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Authorize/AuthorizeEndpointBaseTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Authorize/AuthorizeEndpointBaseTests.cs index ef0221745..9693e82f9 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Authorize/AuthorizeEndpointBaseTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Authorize/AuthorizeEndpointBaseTests.cs @@ -5,8 +5,7 @@ using System.Security.Claims; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; -using Open.IdentityServer; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer.Configuration; using Open.IdentityServer.Endpoints; using Open.IdentityServer.Endpoints.Results; @@ -17,9 +16,11 @@ using Open.IdentityServer.Validation; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; +using Moq; +using Open.IdentityServer.Events; using Xunit; -namespace IdentityServer.UnitTests.Endpoints.Authorize; +namespace Open.IdentityServer.UnitTests.Endpoints.Authorize; public class AuthorizeEndpointBaseTests { @@ -49,6 +50,8 @@ public class AuthorizeEndpointBaseTests private ValidatedAuthorizeRequest _validatedAuthorizeRequest; + private Mock _telemetry; + public AuthorizeEndpointBaseTests() { Init(); @@ -157,6 +160,118 @@ public async Task successful_authorization_request_should_generate_authorize_res result.Should().BeOfType(); } + + [Fact] + [Trait("Category", Category)] + public async Task successful_authorization_request_should_raise_token_issued_success_event() + { + var result = await _subject.ProcessAuthorizeRequestAsync(_params, _user, null); + + result.Should().BeOfType(); + var evt = _fakeEventService.AssertEventWasRaised(); + evt.ClientId.Should().Be(_validatedAuthorizeRequest.ClientId); + } + + [Fact] + [Trait("Category", Category)] + public async Task authorize_request_validation_produces_error_should_raise_token_issued_failure_event() + { + _stubAuthorizeRequestValidator.Result.IsError = true; + _stubAuthorizeRequestValidator.Result.Error = "some_error"; + + var result = await _subject.ProcessAuthorizeRequestAsync(_params, _user, null); + + result.Should().BeOfType(); + var evt = _fakeEventService.AssertEventWasRaised(); + evt.Error.Should().Be("some_error"); + } + + [Fact] + [Trait("Category", Category)] + public async Task interaction_produces_error_should_raise_token_issued_failure_event() + { + _stubInteractionGenerator.Response.Error = "interaction_error"; + _stubInteractionGenerator.Response.ErrorDescription = "something went wrong"; + + var result = await _subject.ProcessAuthorizeRequestAsync(_params, _user, null); + + result.Should().BeOfType(); + var evt = _fakeEventService.AssertEventWasRaised(); + evt.Error.Should().Be("interaction_error"); + evt.ErrorDescription.Should().Be("something went wrong"); + } + + [Fact] + [Trait("Category", Category)] + public async Task authorize_response_is_error_should_raise_token_issued_failure_event() + { + _stubAuthorizeResponseGenerator.Response.Error = "access_denied"; + _stubAuthorizeResponseGenerator.Response.ErrorDescription = "user denied access"; + + var result = await _subject.ProcessAuthorizeRequestAsync(_params, _user, null); + + result.Should().BeOfType(); + var evt = _fakeEventService.AssertEventWasRaised(); + evt.Error.Should().Be("access_denied"); + evt.ErrorDescription.Should().Be("user denied access"); + } + + [Fact] + [Trait("Category", Category)] + public async Task successful_authorization_request_should_emit_telemetry_signal() + { + _telemetry.Setup(t => t.CountTokenIssued("client", "grant_type")) + .Verifiable(Times.Once); + + await _subject.ProcessAuthorizeRequestAsync(_params, _user, null); + + _telemetry.Verify(); + } + + [Fact] + [Trait("Category", Category)] + public async Task authorize_request_validation_produces_error_should_emit_telemetry_signal() + { + _telemetry.Setup(t => t.CountTokenIssued("client", "grant_type", "some_error")) + .Verifiable(Times.Once); + + _stubAuthorizeRequestValidator.Result.IsError = true; + _stubAuthorizeRequestValidator.Result.Error = "some_error"; + + await _subject.ProcessAuthorizeRequestAsync(_params, _user, null); + + _telemetry.Verify(); + } + + [Fact] + [Trait("Category", Category)] + public async Task interaction_produces_error_should_emit_telemetry_signal() + { + _telemetry.Setup(t => t.CountTokenIssued("client", "grant_type", "interaction_error")) + .Verifiable(Times.Once); + + _stubInteractionGenerator.Response.Error = "interaction_error"; + _stubInteractionGenerator.Response.ErrorDescription = "something went wrong"; + + await _subject.ProcessAuthorizeRequestAsync(_params, _user, null); + + _telemetry.Verify(); + } + + [Fact] + [Trait("Category", Category)] + public async Task authorize_response_is_error_should_emit_telemetry_signal() + { + _telemetry.Setup(t => t.CountTokenIssued("client", "grant_type", "access_denied")) + .Verifiable(Times.Once); + + _stubAuthorizeResponseGenerator.Response.Error = "access_denied"; + _stubAuthorizeResponseGenerator.Response.ErrorDescription = "user denied access"; + + await _subject.ProcessAuthorizeRequestAsync(_params, _user, null); + + _telemetry.Verify(); + } internal void Init() { @@ -174,12 +289,15 @@ internal void Init() ClientName = "Test Client" }, Raw = _params, - Subject = _user + Subject = _user, + GrantType = "grant_type", }; _stubAuthorizeResponseGenerator.Response.Request = _validatedAuthorizeRequest; _stubAuthorizeRequestValidator.Result = new AuthorizeRequestValidationResult(_validatedAuthorizeRequest); + _telemetry = new(); + _subject = new TestAuthorizeEndpoint( _fakeEventService, _fakeLogger, @@ -187,7 +305,8 @@ internal void Init() _stubAuthorizeRequestValidator, _stubInteractionGenerator, _stubAuthorizeResponseGenerator, - _mockUserSession); + _mockUserSession, + _telemetry.Object); } internal class TestAuthorizeEndpoint : AuthorizeEndpointBase @@ -199,8 +318,9 @@ public TestAuthorizeEndpoint( IAuthorizeRequestValidator validator, IAuthorizeInteractionResponseGenerator interactionGenerator, IAuthorizeResponseGenerator authorizeResponseGenerator, - IUserSession userSession) - : base(events, logger, options, validator, interactionGenerator, authorizeResponseGenerator, userSession) + IUserSession userSession, + ITelemetryService telemetry) + : base(events, logger, options, validator, interactionGenerator, authorizeResponseGenerator, userSession, telemetry) { } diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Authorize/AuthorizeEndpointTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Authorize/AuthorizeEndpointTests.cs index f266bda13..81874329b 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Authorize/AuthorizeEndpointTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Authorize/AuthorizeEndpointTests.cs @@ -6,8 +6,7 @@ using System.Security.Claims; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; -using Open.IdentityServer; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer.Configuration; using Open.IdentityServer.Endpoints; using Open.IdentityServer.Endpoints.Results; @@ -15,9 +14,11 @@ using Open.IdentityServer.Validation; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; +using Moq; +using Open.IdentityServer.Services; using Xunit; -namespace IdentityServer.UnitTests.Endpoints.Authorize; +namespace Open.IdentityServer.UnitTests.Endpoints.Authorize; public class AuthorizeEndpointTests { @@ -46,6 +47,8 @@ public class AuthorizeEndpointTests private ClaimsPrincipal _user = new IdentityServerUser("bob").CreatePrincipal(); private ValidatedAuthorizeRequest _validatedAuthorizeRequest; + private Mock _telemetry; + private Mock _trace; public AuthorizeEndpointTests() { @@ -78,9 +81,26 @@ public async Task ProcessAsync_post_without_form_content_type_should_return_415( statusCode.StatusCode.Should().Be(415); } + [Fact] + [Trait("Category", Category)] + public async Task ProcessAsync_should_initiate_telemetry_trace() + { + await _subject.ProcessAsync(_context); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Basic, + _subject, + "ProcessAsync"), Times.Once); + _trace.Verify(t => t.Dispose(), Times.Once); + } + internal void Init() { _context = new MockHttpContextAccessor().HttpContext; + _telemetry = new(); + _trace = new(); + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); _validatedAuthorizeRequest = new ValidatedAuthorizeRequest() { @@ -107,6 +127,7 @@ internal void Init() _stubAuthorizeRequestValidator, _stubInteractionGenerator, _stubAuthorizeResponseGenerator, - _mockUserSession); + _mockUserSession, + _telemetry.Object); } } \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Authorize/StubAuthorizeInteractionResponseGenerator.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Authorize/StubAuthorizeInteractionResponseGenerator.cs index 0f97b46ba..81e4d4d35 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Authorize/StubAuthorizeInteractionResponseGenerator.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Authorize/StubAuthorizeInteractionResponseGenerator.cs @@ -7,7 +7,7 @@ using Open.IdentityServer.ResponseHandling; using Open.IdentityServer.Validation; -namespace IdentityServer.UnitTests.Endpoints.Authorize; +namespace Open.IdentityServer.UnitTests.Endpoints.Authorize; internal class StubAuthorizeInteractionResponseGenerator : IAuthorizeInteractionResponseGenerator { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Authorize/StubAuthorizeRequestValidator.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Authorize/StubAuthorizeRequestValidator.cs index 52b1e361e..e1ae2c2eb 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Authorize/StubAuthorizeRequestValidator.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Authorize/StubAuthorizeRequestValidator.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using Open.IdentityServer.Validation; -namespace IdentityServer.UnitTests.Endpoints.Authorize; +namespace Open.IdentityServer.UnitTests.Endpoints.Authorize; public class StubAuthorizeRequestValidator : IAuthorizeRequestValidator { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/CheckSessionEndpointTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/CheckSessionEndpointTests.cs new file mode 100644 index 000000000..c9b616164 --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/CheckSessionEndpointTests.cs @@ -0,0 +1,48 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Moq; +using Open.IdentityServer.Endpoints; +using Open.IdentityServer.Services; +using Open.IdentityServer.UnitTests.Common; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Endpoints; + +public class CheckSessionEndpointTests +{ + private Mock _telemetry; + private Mock _trace; + + public CheckSessionEndpointTests() + { + _telemetry = new Mock(); + _trace = new Mock(); + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + } + + private CheckSessionEndpoint CreateSubject() + { + return new CheckSessionEndpoint( + _telemetry.Object, + TestLogger.Create()); + } + + [Fact] + public async Task process_should_initiate_telemetry_trace() + { + var subject = CreateSubject(); + var context = new DefaultHttpContext(); + + await subject.ProcessAsync(context); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Basic, + subject, + "ProcessAsync"), Times.Once); + _trace.Verify(t => t.Dispose(), Times.Once); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/DeviceAuthorizationEndpointTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/DeviceAuthorizationEndpointTests.cs new file mode 100644 index 000000000..9619c749f --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/DeviceAuthorizationEndpointTests.cs @@ -0,0 +1,321 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Collections.Specialized; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using AwesomeAssertions; +using Open.IdentityServer.UnitTests.Common; +using Microsoft.AspNetCore.Http; +using Moq; +using Open.IdentityServer.Endpoints; +using Open.IdentityServer.Endpoints.Results; +using Open.IdentityServer.Events; +using Open.IdentityServer.ResponseHandling; +using Open.IdentityServer.Services; +using Open.IdentityServer.Validation; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Endpoints; + +public class DeviceAuthorizationEndpointTests +{ + private const string Category = "Endpoints"; + + private readonly Mock _clientValidator = new(); + private readonly Mock _requestValidator = new(); + private readonly Mock _responseGenerator = new(); + private readonly TestEventService _events = new(); + private readonly Mock _telemetry = new(); + private readonly Mock _trace = new(); + + public DeviceAuthorizationEndpointTests() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + } + + private DeviceAuthorizationEndpoint CreateSubject() + { + return new DeviceAuthorizationEndpoint( + _clientValidator.Object, + _requestValidator.Object, + _responseGenerator.Object, + _events, + _telemetry.Object, + TestLogger.Create()); + } + + private HttpContext CreatePostContext(string body = "") + { + var context = new DefaultHttpContext(); + context.Request.Method = HttpMethods.Post; + context.Request.ContentType = "application/x-www-form-urlencoded"; + + var stream = new MemoryStream(Encoding.UTF8.GetBytes(body)); + context.Request.Body = stream; + + return context; + } + + [Fact] + [Trait("Category", Category)] + public async Task process_should_return_error_for_get_request() + { + var subject = CreateSubject(); + var context = new DefaultHttpContext(); + context.Request.Method = HttpMethods.Get; + + var result = await subject.ProcessAsync(context); + + result.Should().BeOfType(); + var errorResult = result as TokenErrorResult; + errorResult.Response.Error.Should().Be(OidcConstants.TokenErrors.InvalidRequest); + } + + [Fact] + [Trait("Category", Category)] + public async Task process_should_return_error_for_missing_content_type() + { + var subject = CreateSubject(); + var context = new DefaultHttpContext(); + context.Request.Method = HttpMethods.Post; + context.Request.ContentType = "application/json"; + + var result = await subject.ProcessAsync(context); + + result.Should().BeOfType(); + var errorResult = result as TokenErrorResult; + errorResult.Response.Error.Should().Be(OidcConstants.TokenErrors.InvalidRequest); + } + + [Fact] + [Trait("Category", Category)] + public async Task process_should_return_error_when_client_validation_fails() + { + var subject = CreateSubject(); + var context = CreatePostContext(); + + _clientValidator.Setup(x => x.ValidateAsync(It.IsAny())) + .ReturnsAsync(new ClientSecretValidationResult { IsError = true }); + + var result = await subject.ProcessAsync(context); + + result.Should().BeOfType(); + var errorResult = result as TokenErrorResult; + errorResult.Response.Error.Should().Be(OidcConstants.TokenErrors.InvalidClient); + } + + [Fact] + [Trait("Category", Category)] + public async Task process_should_return_error_when_request_validation_fails() + { + var subject = CreateSubject(); + var context = CreatePostContext(); + var client = new Open.IdentityServer.Models.Client { ClientId = "test-client" }; + + _clientValidator.Setup(x => x.ValidateAsync(It.IsAny())) + .ReturnsAsync(new ClientSecretValidationResult + { + IsError = false, + Client = client + }); + + _requestValidator.Setup(x => + x.ValidateAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((NameValueCollection nvc, ClientSecretValidationResult csrv) => + new DeviceAuthorizationRequestValidationResult + ( + new ValidatedDeviceAuthorizationRequest(){ Raw = nvc }, + error: OidcConstants.TokenErrors.InvalidScope, + errorDescription: "Invalid scope requested" + )); + + var result = await subject.ProcessAsync(context); + + result.Should().BeOfType(); + var errorResult = result as TokenErrorResult; + errorResult.Response.Error.Should().Be(OidcConstants.TokenErrors.InvalidScope); + errorResult.Response.ErrorDescription.Should().Be("Invalid scope requested"); + _events.AssertEventWasRaised(); + } + + [Fact] + [Trait("Category", Category)] + public async Task process_should_raise_telemetry_signal_when_request_validation_fails() + { + _telemetry.Setup(x => x.CountDeviceAuthentication("test-client", OidcConstants.TokenErrors.InvalidScope)) + .Verifiable(Times.Once); + + var subject = CreateSubject(); + var context = CreatePostContext(); + var client = new Open.IdentityServer.Models.Client { ClientId = "test-client" }; + + _clientValidator.Setup(x => x.ValidateAsync(It.IsAny())) + .ReturnsAsync(new ClientSecretValidationResult + { + IsError = false, + Client = client + }); + + _requestValidator.Setup(x => + x.ValidateAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((NameValueCollection nvc, ClientSecretValidationResult csrv) => + new DeviceAuthorizationRequestValidationResult + ( + new ValidatedDeviceAuthorizationRequest(){ Raw = nvc }, + error: OidcConstants.TokenErrors.InvalidScope, + errorDescription: "Invalid scope requested" + )); + + await subject.ProcessAsync(context); + + _telemetry.Verify(); + } + + [Fact] + [Trait("Category", Category)] + public async Task process_should_return_device_authorization_result_on_success() + { + var subject = CreateSubject(); + var context = CreatePostContext(); + var client = new Open.IdentityServer.Models.Client { ClientId = "test-client" }; + + var validatedRequest = + new DeviceAuthorizationRequestValidationResult(new ValidatedDeviceAuthorizationRequest { Client = client }); + + var response = new DeviceAuthorizationResponse + { + DeviceCode = "test-device-code", + UserCode = "TEST-USER-CODE", + VerificationUri = "https://example.com/device", + DeviceCodeLifetime = 1800 + }; + + _clientValidator.Setup(x => x.ValidateAsync(It.IsAny())) + .ReturnsAsync(new ClientSecretValidationResult + { + IsError = false, + Client = client + }); + + _requestValidator.Setup(x => x.ValidateAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(validatedRequest); + + _responseGenerator.Setup(x => x.ProcessAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(response); + + var result = await subject.ProcessAsync(context); + + result.Should().BeOfType(); + var deviceResult = result as DeviceAuthorizationResult; + deviceResult.Response.DeviceCode.Should().Be("test-device-code"); + deviceResult.Response.UserCode.Should().Be("TEST-USER-CODE"); + _events.AssertEventWasRaised(); + } + + [Fact] + [Trait("Category", Category)] + public async Task process_should_raise_telemetry_result_on_success() + { + _telemetry.Setup(x => x.CountDeviceAuthentication("test-client")) + .Verifiable(Times.Once); + + var subject = CreateSubject(); + var context = CreatePostContext(); + var client = new Open.IdentityServer.Models.Client { ClientId = "test-client" }; + + var validatedRequest = + new DeviceAuthorizationRequestValidationResult(new ValidatedDeviceAuthorizationRequest { Client = client }); + + var response = new DeviceAuthorizationResponse + { + DeviceCode = "test-device-code", + UserCode = "TEST-USER-CODE", + VerificationUri = "https://example.com/device", + DeviceCodeLifetime = 1800 + }; + + _clientValidator.Setup(x => x.ValidateAsync(It.IsAny())) + .ReturnsAsync(new ClientSecretValidationResult + { + IsError = false, + Client = client + }); + + _requestValidator.Setup(x => x.ValidateAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(validatedRequest); + + _responseGenerator.Setup(x => x.ProcessAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(response); + + await subject.ProcessAsync(context); + + _telemetry.Verify(); + } + + [Fact] + [Trait("Category", Category)] + public async Task process_should_pass_base_url_to_response_generator() + { + var subject = CreateSubject(); + var context = CreatePostContext(); + context.Request.Scheme = "https"; + context.Request.Host = new HostString("example.com"); + context.Request.PathBase = "/auth"; + + var client = new Open.IdentityServer.Models.Client { ClientId = "test-client" }; + var validatedRequest = new DeviceAuthorizationRequestValidationResult(new ValidatedDeviceAuthorizationRequest { Client = client }); + + var response = new DeviceAuthorizationResponse { DeviceCode = "code" }; + + _clientValidator.Setup(x => x.ValidateAsync(It.IsAny())) + .ReturnsAsync(new ClientSecretValidationResult { IsError = false, Client = client }); + + _requestValidator.Setup(x => x.ValidateAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(validatedRequest); + + string capturedBaseUrl = null; + _responseGenerator.Setup(x => x.ProcessAsync(It.IsAny(), It.IsAny())) + .Callback((_, baseUrl) => capturedBaseUrl = baseUrl) + .ReturnsAsync(response); + + await subject.ProcessAsync(context); + + capturedBaseUrl.Should().Be("https://example.com/"); + } + + [Fact] + [Trait("Category", Category)] + public async Task process_should_initiate_telemetry_trace() + { + var subject = CreateSubject(); + var context = CreatePostContext(); + context.Request.Scheme = "https"; + context.Request.Host = new HostString("example.com"); + context.Request.PathBase = "/auth"; + + var client = new Open.IdentityServer.Models.Client { ClientId = "test-client" }; + var validatedRequest = new DeviceAuthorizationRequestValidationResult(new ValidatedDeviceAuthorizationRequest { Client = client }); + + var response = new DeviceAuthorizationResponse { DeviceCode = "code" }; + + _clientValidator.Setup(x => x.ValidateAsync(It.IsAny())) + .ReturnsAsync(new ClientSecretValidationResult { IsError = false, Client = client }); + + _requestValidator.Setup(x => x.ValidateAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(validatedRequest); + + _responseGenerator.Setup(x => x.ProcessAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(response); + + await subject.ProcessAsync(context); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Basic, + subject, + "ProcessAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/DiscoveryEndpointTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/DiscoveryEndpointTests.cs new file mode 100644 index 000000000..aac35fe73 --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/DiscoveryEndpointTests.cs @@ -0,0 +1,57 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Moq; +using Open.IdentityServer.Configuration; +using Open.IdentityServer.Endpoints; +using Open.IdentityServer.ResponseHandling; +using Open.IdentityServer.Services; +using Open.IdentityServer.UnitTests.Common; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Endpoints; + +public class DiscoveryEndpointTests +{ + private IdentityServerOptions _options; + private Mock _responseGenerator; + private Mock _telemetry; + private Mock _trace; + + public DiscoveryEndpointTests() + { + _options = new(); + _responseGenerator = new(); + _telemetry = new(); + _trace = new(); + } + + private DiscoveryEndpoint CreateSubject() + { + return new DiscoveryEndpoint( + _options, + _responseGenerator.Object, + _telemetry.Object, + TestLogger.Create()); + } + + [Fact] + public async Task Process_WhenCalled_ShouldTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var subject = CreateSubject(); + var context = new DefaultHttpContext(); + + await subject.ProcessAsync(context); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Basic, + subject, + "ProcessAsync"), Times.Once); + _trace.Verify(t => t.Dispose(), Times.Once); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/DiscoveryKeyEndpointTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/DiscoveryKeyEndpointTests.cs new file mode 100644 index 000000000..8a6f85eb7 --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/DiscoveryKeyEndpointTests.cs @@ -0,0 +1,60 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Moq; +using Open.IdentityServer.Configuration; +using Open.IdentityServer.Endpoints; +using Open.IdentityServer.ResponseHandling; +using Open.IdentityServer.Services; +using Open.IdentityServer.UnitTests.Common; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Endpoints; + +public class DiscoveryKeyEndpointTests +{ + private IdentityServerOptions _options; + private Mock _responseGenerator; + private Mock _telemetry; + private Mock _trace; + + + public DiscoveryKeyEndpointTests() + { + _options = new(); + _responseGenerator = new(); + _telemetry = new(); + _trace = new(); + } + + private DiscoveryKeyEndpoint CreateSubject() + { + return new DiscoveryKeyEndpoint( + _options, + _responseGenerator.Object, + _telemetry.Object, + TestLogger.Create() + ); + } + + [Fact] + public async Task Process_WhenCalled_ShouldTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var subject = CreateSubject(); + var context = new DefaultHttpContext(); + + await subject.ProcessAsync(context); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Basic, + subject, + "ProcessAsync") + ); + _trace.Verify(t => t.Dispose(), Times.Once); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/EndSession/EndSessionCallbackEndpointTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/EndSession/EndSessionCallbackEndpointTests.cs index 4387724ed..a63f1fc0e 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/EndSession/EndSessionCallbackEndpointTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/EndSession/EndSessionCallbackEndpointTests.cs @@ -1,23 +1,48 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. +using System.ComponentModel; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Open.IdentityServer.Endpoints; using Microsoft.Extensions.Logging; +using Moq; +using Open.IdentityServer.Services; +using Xunit; -namespace IdentityServer.UnitTests.Endpoints.EndSession; +namespace Open.IdentityServer.UnitTests.Endpoints.EndSession; public class EndSessionCallbackEndpointTests { private const string Category = "End Session Callback Endpoint"; - StubEndSessionRequestValidator _stubEndSessionRequestValidator = new StubEndSessionRequestValidator(); - EndSessionCallbackEndpoint _subject; + private StubEndSessionRequestValidator _stubEndSessionRequestValidator = new StubEndSessionRequestValidator(); + private EndSessionCallbackEndpoint _subject; + private Mock _telemetry = new (); + private Mock _trace = new (); public EndSessionCallbackEndpointTests() { _subject = new EndSessionCallbackEndpoint( _stubEndSessionRequestValidator, + _telemetry.Object, new LoggerFactory().CreateLogger()); } + + [Fact] + [Trait("Category", Category)] + public async Task ProcessAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var context = new DefaultHttpContext(); + + await _subject.ProcessAsync(context); + + _telemetry.Verify(t => t.Trace(TelemetryConstants.TraceCategories.Basic, _subject, "ProcessAsync"), Times.Once); + _trace.Verify(t => t.Dispose(), Times.Once); + } } \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/EndSession/EndSessionCallbackResultTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/EndSession/EndSessionCallbackResultTests.cs index 059cd3b15..0b412dbdd 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/EndSession/EndSessionCallbackResultTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/EndSession/EndSessionCallbackResultTests.cs @@ -11,7 +11,7 @@ using Microsoft.AspNetCore.Http; using Xunit; -namespace IdentityServer.UnitTests.Endpoints.EndSession; +namespace Open.IdentityServer.UnitTests.Endpoints.EndSession; public class EndSessionCallbackResultTests { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/EndSession/StubBackChannelLogoutClient.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/EndSession/StubBackChannelLogoutClient.cs index d9db6202d..0da055f41 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/EndSession/StubBackChannelLogoutClient.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/EndSession/StubBackChannelLogoutClient.cs @@ -6,7 +6,7 @@ using Open.IdentityServer.Models; using Open.IdentityServer.Services; -namespace IdentityServer.UnitTests.Endpoints.EndSession; +namespace Open.IdentityServer.UnitTests.Endpoints.EndSession; internal class StubBackChannelLogoutClient : IBackChannelLogoutService { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/EndSession/StubEndSessionRequestValidator.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/EndSession/StubEndSessionRequestValidator.cs index 135cfb425..2416b16af 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/EndSession/StubEndSessionRequestValidator.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/EndSession/StubEndSessionRequestValidator.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using Open.IdentityServer.Validation; -namespace IdentityServer.UnitTests.Endpoints.EndSession; +namespace Open.IdentityServer.UnitTests.Endpoints.EndSession; class StubEndSessionRequestValidator : IEndSessionRequestValidator { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/EndSessionEndpointTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/EndSessionEndpointTests.cs new file mode 100644 index 000000000..19588465e --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/EndSessionEndpointTests.cs @@ -0,0 +1,52 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Moq; +using Open.IdentityServer.Endpoints; +using Open.IdentityServer.Services; +using Open.IdentityServer.UnitTests.Common; +using Open.IdentityServer.Validation; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Endpoints; + +public class EndSessionEndpointTests +{ + private Mock _endSessionRequestValidator = new(); + private Mock _userSession = new(); + private Mock _telemetry = new(); + private Mock _trace = new(); + + public EndSessionEndpointTests() + { + } + + private EndSessionEndpoint CreateSubject() + { + return new EndSessionEndpoint( + _endSessionRequestValidator.Object, + _userSession.Object, + _telemetry.Object, + TestLogger.Create()); + } + + [Fact] + public async Task process_should_initiate_telemetry_trace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var subject = CreateSubject(); + var context = new DefaultHttpContext(); + + await subject.ProcessAsync(context); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Basic, + subject, + "ProcessAsync"), Times.Once); + _trace.Verify(t => t.Dispose(), Times.Once); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/IntrospectionEndpointTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/IntrospectionEndpointTests.cs new file mode 100644 index 000000000..7f3964086 --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/IntrospectionEndpointTests.cs @@ -0,0 +1,135 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Collections.Specialized; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using AwesomeAssertions; +using Open.IdentityServer.UnitTests.Common; +using Microsoft.AspNetCore.Http; +using Moq; +using Open.IdentityServer.Endpoints; +using Open.IdentityServer.Endpoints.Results; +using Open.IdentityServer.Events; +using Open.IdentityServer.Models; +using Open.IdentityServer.ResponseHandling; +using Open.IdentityServer.Services; +using Open.IdentityServer.Validation; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Endpoints; + +public class IntrospectionEndpointTests +{ + private const string Category = "Endpoints - Introspection"; + + private readonly Mock _apiSecretValidator = new(); + private readonly Mock _requestValidator = new(); + private readonly Mock _responseGenerator = new(); + private readonly TestEventService _events = new(); + private readonly Mock _telemetryService = new(); + private readonly Mock _trace = new(); + + private IntrospectionEndpoint CreateSubject() + { + return new IntrospectionEndpoint( + _apiSecretValidator.Object, + _requestValidator.Object, + _responseGenerator.Object, + _telemetryService.Object, + _events, + TestLogger.Create()); + } + + private static HttpContext CreatePostFormContext(string formBody = "token=abc") + { + var context = new DefaultHttpContext(); + context.Request.Method = HttpMethods.Post; + context.Request.ContentType = "application/x-www-form-urlencoded"; + context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(formBody)); + return context; + } + + [Fact] + [Trait("Category", Category)] + public async Task process_should_raise_failure_event_when_request_validation_fails() + { + var subject = CreateSubject(); + var context = CreatePostFormContext("token=abc"); + var api = new ApiResource("api1"); + + _apiSecretValidator.Setup(x => x.ValidateAsync(It.IsAny())) + .ReturnsAsync(new ApiSecretValidationResult + { + IsError = false, + Resource = api + }); + + _requestValidator.Setup(x => x.ValidateAsync(It.IsAny(), api)) + .ReturnsAsync(new IntrospectionRequestValidationResult + { + IsError = true, + Error = "invalid_request", + Api = api + }); + + var result = await subject.ProcessAsync(context); + + result.Should().BeOfType(); + _events.AssertEventWasRaised(); + } + + [Fact] + [Trait("Category", Category)] + public async Task process_should_send_telemetry_signal_when_request_validation_fails() + { + _telemetryService.Setup(t => t.CountTokenIntrospection("api1", null, "invalid_request")) + .Verifiable(Times.Once); + + var subject = CreateSubject(); + var context = CreatePostFormContext("token=abc"); + var api = new ApiResource("api1"); + + _apiSecretValidator.Setup(x => x.ValidateAsync(It.IsAny())) + .ReturnsAsync(new ApiSecretValidationResult + { + IsError = false, + Resource = api + }); + + _requestValidator.Setup(x => x.ValidateAsync(It.IsAny(), api)) + .ReturnsAsync(new IntrospectionRequestValidationResult + { + IsError = true, + Error = "invalid_request", + Api = api + }); + + await subject.ProcessAsync(context); + + _telemetryService.Verify(); + } + + [Fact] + [Trait("Category", Category)] + public async Task process_should_initiate_telemetry_trace() + { + _telemetryService.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var subject = CreateSubject(); + var context = CreatePostFormContext("token=abc"); + + _apiSecretValidator.Setup(x => x.ValidateAsync(It.IsAny())) + .ReturnsAsync(new ApiSecretValidationResult { }); + + await subject.ProcessAsync(context); + + _telemetryService.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Basic, + subject, + "ProcessAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Results/AuthorizeResultTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Results/AuthorizeResultTests.cs index 4cbe3ff12..7aee5a7e7 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Results/AuthorizeResultTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Results/AuthorizeResultTests.cs @@ -8,7 +8,7 @@ using System.Linq; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer.Configuration; using Open.IdentityServer.Endpoints.Results; using Open.IdentityServer.Extensions; @@ -21,7 +21,7 @@ using Open.IdentityServer; using Xunit; -namespace IdentityServer.UnitTests.Endpoints.Results; +namespace Open.IdentityServer.UnitTests.Endpoints.Results; public class AuthorizeResultTests { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Results/CheckSessionResultTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Results/CheckSessionResultTests.cs index ea62f16ab..5d6cd25f4 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Results/CheckSessionResultTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Results/CheckSessionResultTests.cs @@ -13,7 +13,7 @@ using Microsoft.AspNetCore.Http; using Xunit; -namespace IdentityServer.UnitTests.Endpoints.Results; +namespace Open.IdentityServer.UnitTests.Endpoints.Results; public class CheckSessionResultTests { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Results/EndSessionCallbackResultTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Results/EndSessionCallbackResultTests.cs index a3936c49f..e04eebff2 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Results/EndSessionCallbackResultTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Results/EndSessionCallbackResultTests.cs @@ -6,7 +6,7 @@ using System.Linq; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer.Configuration; using Open.IdentityServer.Endpoints.Results; using Open.IdentityServer.Extensions; @@ -15,7 +15,7 @@ using Microsoft.AspNetCore.Http; using Xunit; -namespace IdentityServer.UnitTests.Endpoints.Results; +namespace Open.IdentityServer.UnitTests.Endpoints.Results; public class EndSessionCallbackResultTests { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Results/EndSessionResultTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Results/EndSessionResultTests.cs index 27a8c6af6..48ebde47f 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Results/EndSessionResultTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/Results/EndSessionResultTests.cs @@ -6,7 +6,7 @@ using System.Linq; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer.Configuration; using Open.IdentityServer.Endpoints.Results; using Open.IdentityServer.Extensions; @@ -16,7 +16,7 @@ using Microsoft.AspNetCore.WebUtilities; using Xunit; -namespace IdentityServer.UnitTests.Endpoints.Results; +namespace Open.IdentityServer.UnitTests.Endpoints.Results; public class EndSessionResultTests { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/TokenEndpointTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/TokenEndpointTests.cs new file mode 100644 index 000000000..025b00448 --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/TokenEndpointTests.cs @@ -0,0 +1,188 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.IO; +using System.Text; +using System.Threading.Tasks; +using AwesomeAssertions; +using Open.IdentityServer.UnitTests.Common; +using Microsoft.AspNetCore.Http; +using Moq; +using Open.IdentityServer.Endpoints; +using Open.IdentityServer.Endpoints.Results; +using Open.IdentityServer.Events; +using Open.IdentityServer.Models; +using Open.IdentityServer.ResponseHandling; +using Open.IdentityServer.Services; +using Open.IdentityServer.Validation; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Endpoints; + +public class TokenEndpointTests +{ + private const string Category = "Endpoints - Token"; + + private readonly Mock _clientValidator = new(); + private readonly Mock _requestValidator = new(); + private readonly Mock _responseGenerator = new(); + private readonly Mock _telemetry = new(); + private readonly Mock _trace = new(); + private readonly TestEventService _events = new(); + + private TokenEndpoint CreateSubject() => + new TokenEndpoint( + _clientValidator.Object, + _requestValidator.Object, + _responseGenerator.Object, + _events, + _telemetry.Object, + TestLogger.Create()); + + private static HttpContext CreatePostFormContext(string formBody = "grant_type=client_credentials") + { + var context = new DefaultHttpContext(); + context.Request.Method = HttpMethods.Post; + context.Request.ContentType = "application/x-www-form-urlencoded"; + context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(formBody)); + return context; + } + + private static ClientSecretValidationResult ValidClientResult() => + new ClientSecretValidationResult + { + Client = new Client { ClientId = "client", ClientName = "Test Client" } + }; + + private static TokenRequestValidationResult ValidRequestResult() => + new TokenRequestValidationResult( + new ValidatedTokenRequest + { + Client = new Client { ClientId = "client", ClientName = "Test Client" }, + GrantType = OidcConstants.GrantTypes.ClientCredentials + }); + + private static TokenRequestValidationResult FailedRequestResult(string error, string errorDescription = null) => + new TokenRequestValidationResult( + new ValidatedTokenRequest + { + Client = new Client { ClientId = "client", ClientName = "Test Client" }, + GrantType = OidcConstants.GrantTypes.ClientCredentials + }, + error, + errorDescription); + + [Fact] + [Trait("Category", Category)] + public async Task process_should_raise_token_issued_success_event_when_request_is_valid() + { + var subject = CreateSubject(); + var context = CreatePostFormContext(); + + _clientValidator.Setup(x => x.ValidateAsync(It.IsAny())) + .ReturnsAsync(ValidClientResult()); + + _requestValidator.Setup(x => x.ValidateRequestAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(ValidRequestResult()); + + _responseGenerator.Setup(x => x.ProcessAsync(It.IsAny())) + .ReturnsAsync(new TokenResponse { AccessToken = "access_token" }); + + var result = await subject.ProcessAsync(context); + + result.Should().BeOfType(); + _events.AssertEventWasRaised(); + } + + [Fact] + [Trait("Category", Category)] + public async Task process_should_raise_token_issued_failure_event_when_request_validation_fails() + { + var subject = CreateSubject(); + var context = CreatePostFormContext(); + + _clientValidator.Setup(x => x.ValidateAsync(It.IsAny())) + .ReturnsAsync(ValidClientResult()); + + _requestValidator.Setup(x => x.ValidateRequestAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(FailedRequestResult(OidcConstants.TokenErrors.InvalidGrant, "invalid_username_or_password")); + + var result = await subject.ProcessAsync(context); + + result.Should().BeOfType(); + var evt = _events.AssertEventWasRaised(); + evt.Error.Should().Be(OidcConstants.TokenErrors.InvalidGrant); + evt.ErrorDescription.Should().Be("invalid_username_or_password"); + evt.ClientId.Should().Be("client"); + } + + [Fact] + [Trait("Category", Category)] + public async Task process_should_emit_telemetry_signal_when_request_is_valid() + { + _telemetry.Setup(t => t.CountTokenIssued("client", "client_credentials")) + .Verifiable(Times.Once); + + var subject = CreateSubject(); + var context = CreatePostFormContext(); + + _clientValidator.Setup(x => x.ValidateAsync(It.IsAny())) + .ReturnsAsync(ValidClientResult()); + + _requestValidator.Setup(x => x.ValidateRequestAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(ValidRequestResult()); + + _responseGenerator.Setup(x => x.ProcessAsync(It.IsAny())) + .ReturnsAsync(new TokenResponse { AccessToken = "access_token" }); + + await subject.ProcessAsync(context); + + _telemetry.Verify(); + } + + [Fact] + [Trait("Category", Category)] + public async Task process_should_emit_telemetry_signal_when_request_validation_fails() + { + _telemetry.Setup(t => t.CountTokenIssued("client", "client_credentials", "invalid_grant")) + .Verifiable(Times.Once); + + var subject = CreateSubject(); + var context = CreatePostFormContext(); + + _clientValidator.Setup(x => x.ValidateAsync(It.IsAny())) + .ReturnsAsync(ValidClientResult()); + + _requestValidator.Setup(x => x.ValidateRequestAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(FailedRequestResult(OidcConstants.TokenErrors.InvalidGrant, "invalid_username_or_password")); + + await subject.ProcessAsync(context); + + _telemetry.Verify(); + } + + [Fact] + [Trait("Category", Category)] + public async Task process_should_initiate_telemetry_trace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var subject = CreateSubject(); + var context = CreatePostFormContext(); + + _clientValidator.Setup(x => x.ValidateAsync(It.IsAny())) + .ReturnsAsync(ValidClientResult()); + + _requestValidator.Setup(x => x.ValidateRequestAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(ValidRequestResult()); + + _responseGenerator.Setup(x => x.ProcessAsync(It.IsAny())) + .ReturnsAsync(new TokenResponse { AccessToken = "access_token" }); + + await subject.ProcessAsync(context); + + _telemetry.Verify(t => t.Trace(TelemetryConstants.TraceCategories.Basic, subject, "ProcessAsync"), Times.Once); + _trace.Verify(t => t.Dispose(), Times.Once); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/TokenRevocationEndpointTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/TokenRevocationEndpointTests.cs new file mode 100644 index 000000000..9c68548da --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/TokenRevocationEndpointTests.cs @@ -0,0 +1,193 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.IO; +using System.Text; +using System.Threading.Tasks; +using AwesomeAssertions; +using Open.IdentityServer.UnitTests.Common; +using Microsoft.AspNetCore.Http; +using Moq; +using Open.IdentityServer.Endpoints; +using Open.IdentityServer.Endpoints.Results; +using Open.IdentityServer.Events; +using Open.IdentityServer.Models; +using Open.IdentityServer.ResponseHandling; +using Open.IdentityServer.Services; +using Open.IdentityServer.Validation; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Endpoints; + +public class TokenRevocationEndpointTests +{ + private const string Category = "Endpoints - Token Revocation"; + + private readonly Mock _clientValidator = new(); + private readonly Mock _requestValidator = new(); + private readonly Mock _responseGenerator = new(); + private readonly Mock _telemetry = new(); + private readonly Mock _trace = new(); + private readonly TestEventService _events = new(); + + private TokenRevocationEndpoint CreateSubject() => + new TokenRevocationEndpoint( + TestLogger.Create(), + _clientValidator.Object, + _requestValidator.Object, + _responseGenerator.Object, + _events, + _telemetry.Object); + + private static HttpContext CreatePostFormContext(string formBody = "token=abc") + { + var context = new DefaultHttpContext(); + context.Request.Method = HttpMethods.Post; + context.Request.ContentType = "application/x-www-form-urlencoded"; + context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(formBody)); + return context; + } + + private static Client TestClient() => + new Client { ClientId = "client", ClientName = "Test Client" }; + + private static ClientSecretValidationResult ValidClientResult() => + new ClientSecretValidationResult { Client = TestClient(), IsError = false}; + + private static TokenRevocationRequestValidationResult ValidRequestResult() => + new TokenRevocationRequestValidationResult + { + Client = TestClient(), + Token = "abc", + TokenTypeHint = OidcConstants.TokenTypes.AccessToken, + IsError = false + }; + + [Fact] + [Trait("Category", Category)] + public async Task process_should_raise_token_revoked_success_event_when_token_is_found_and_revoked() + { + var subject = CreateSubject(); + var context = CreatePostFormContext(); + + _clientValidator.Setup(x => x.ValidateAsync(It.IsAny())) + .ReturnsAsync(ValidClientResult()); + + _requestValidator.Setup(x => x.ValidateRequestAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(ValidRequestResult()); + + _responseGenerator.Setup(x => x.ProcessAsync(It.IsAny())) + .ReturnsAsync(new TokenRevocationResponse { Success = true }); + + var result = await subject.ProcessAsync(context); + + var evt = _events.AssertEventWasRaised(); + evt.ClientId.Should().Be("client"); + evt.TokenType.Should().Be(OidcConstants.TokenTypes.AccessToken); + } + + [Fact] + [Trait("Category", Category)] + public async Task process_should_emit_telemetry_signal_when_token_is_found_and_revoked() + { + _telemetry.Setup(t => t.CountTokenRevocation("client")) + .Verifiable(Times.Once); + + var subject = CreateSubject(); + var context = CreatePostFormContext(); + + _clientValidator.Setup(x => x.ValidateAsync(It.IsAny())) + .ReturnsAsync(ValidClientResult()); + + _requestValidator.Setup(x => x.ValidateRequestAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(ValidRequestResult()); + + _responseGenerator.Setup(x => x.ProcessAsync(It.IsAny())) + .ReturnsAsync(new TokenRevocationResponse { Success = true }); + + var result = await subject.ProcessAsync(context); + + _telemetry.Verify(); + } + + [Fact] + [Trait("Category", Category)] + public async Task process_should_emit_telemetry_signal_when_client_validation_fails() + { + _telemetry.Setup(t => t.CountTokenRevocation("client", OidcConstants.TokenErrors.InvalidClient)) + .Verifiable(Times.Once); + + var subject = CreateSubject(); + var context = CreatePostFormContext(); + + var clientResult = ValidClientResult(); + clientResult.IsError = true; + clientResult.Error = OidcConstants.TokenErrors.InvalidClient; + + _clientValidator.Setup(x => x.ValidateAsync(It.IsAny())) + .ReturnsAsync(clientResult); + + _requestValidator.Setup(x => x.ValidateRequestAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(ValidRequestResult()); + + _responseGenerator.Setup(x => x.ProcessAsync(It.IsAny())) + .ReturnsAsync(new TokenRevocationResponse { Success = true }); + + var result = await subject.ProcessAsync(context); + + _telemetry.Verify(); + } + + [Fact] + [Trait("Category", Category)] + public async Task process_should_emit_telemetry_signal_when_request_validation_fails() + { + _telemetry.Setup(t => t.CountTokenRevocation("client", OidcConstants.TokenErrors.InvalidRequest)) + .Verifiable(Times.Once); + + var subject = CreateSubject(); + var context = CreatePostFormContext(); + + _clientValidator.Setup(x => x.ValidateAsync(It.IsAny())) + .ReturnsAsync(ValidClientResult()); + + var requestValidationResult = ValidRequestResult(); + requestValidationResult.IsError = true; + requestValidationResult.Error = OidcConstants.TokenErrors.InvalidRequest; + + _requestValidator.Setup(x => x.ValidateRequestAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(requestValidationResult); + + _responseGenerator.Setup(x => x.ProcessAsync(It.IsAny())) + .ReturnsAsync(new TokenRevocationResponse { Success = true }); + + await subject.ProcessAsync(context); + + _telemetry.Verify(); + } + + [Fact] + [Trait("Category", Category)] + public async Task process_should_trace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var subject = CreateSubject(); + var context = CreatePostFormContext(); + + _clientValidator.Setup(x => x.ValidateAsync(It.IsAny())) + .ReturnsAsync(ValidClientResult()); + + _requestValidator.Setup(x => x.ValidateRequestAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(ValidRequestResult()); + + _responseGenerator.Setup(x => x.ProcessAsync(It.IsAny())) + .ReturnsAsync(new TokenRevocationResponse { Success = true }); + + await subject.ProcessAsync(context); + + _telemetry.Verify(t => t.Trace(TelemetryConstants.TraceCategories.Basic, subject, "ProcessAsync"), Times.Once); + _trace.Verify(t => t.Dispose(), Times.Once); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/UserInfoEndpointTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/UserInfoEndpointTests.cs new file mode 100644 index 000000000..16ce24995 --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Endpoints/UserInfoEndpointTests.cs @@ -0,0 +1,62 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Moq; +using Open.IdentityServer.Endpoints; +using Open.IdentityServer.ResponseHandling; +using Open.IdentityServer.Services; +using Open.IdentityServer.UnitTests.Common; +using Open.IdentityServer.Validation; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Endpoints; + +public class UserInfoEndpointTests +{ + private BearerTokenUsageValidator _bearerTokenUsageValidator; + private Mock _userInfoRequestValidator; + private Mock _userInfoResponseGenerator; + private Mock _telemetry; + private Mock _trace; + + public UserInfoEndpointTests() + { + _bearerTokenUsageValidator = new BearerTokenUsageValidator( + TestLogger.Create()); + _userInfoRequestValidator = new Mock(); + _userInfoResponseGenerator = new Mock(); + _telemetry = new Mock(); + _trace = new Mock(); + } + + private UserInfoEndpoint CreateSubject() + { + return new UserInfoEndpoint( + _bearerTokenUsageValidator, + _userInfoRequestValidator.Object, + _userInfoResponseGenerator.Object, + _telemetry.Object, + TestLogger.Create()); + } + + [Fact] + public async Task ProcessAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var subject = CreateSubject(); + var context = new DefaultHttpContext(); + + await subject.ProcessAsync(context); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Basic, + subject, + "ProcessAsync" + ), Times.Once); + _trace.Verify(t => t.Dispose(), Times.Once); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/ApiResourceSigningAlgorithmSelectionTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/ApiResourceSigningAlgorithmSelectionTests.cs index 5e36f9e27..31164cfba 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/ApiResourceSigningAlgorithmSelectionTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/ApiResourceSigningAlgorithmSelectionTests.cs @@ -8,7 +8,7 @@ using Open.IdentityServer.Models; using Xunit; -namespace IdentityServer.UnitTests.Extensions; +namespace Open.IdentityServer.UnitTests.Extensions; public class ApiResourceSigningAlgorithmSelectionTests { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/AuthorizeResponseExtensionsTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/AuthorizeResponseExtensionsTests.cs index 6f62b6db7..1e15930c8 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/AuthorizeResponseExtensionsTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/AuthorizeResponseExtensionsTests.cs @@ -7,7 +7,7 @@ using Open.IdentityServer.Validation; using Xunit; -namespace IdentityServer.UnitTests.Extensions; +namespace Open.IdentityServer.UnitTests.Extensions; public class AuthorizeResponseExtensionsTests { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/EndpointOptionsExtensionsTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/EndpointOptionsExtensionsTests.cs index 3c74eefde..346ccd797 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/EndpointOptionsExtensionsTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/EndpointOptionsExtensionsTests.cs @@ -7,7 +7,7 @@ using Xunit; using static Open.IdentityServer.Constants; -namespace IdentityServer.UnitTests.Extensions; +namespace Open.IdentityServer.UnitTests.Extensions; public class EndpointOptionsExtensionsTests { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/HashExtensionsTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/HashExtensionsTests.cs index 51b0b9bf0..0358094ed 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/HashExtensionsTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/HashExtensionsTests.cs @@ -5,7 +5,7 @@ using Open.IdentityServer.Models; using Xunit; -namespace IdentityServer.UnitTests.Extensions; +namespace Open.IdentityServer.UnitTests.Extensions; public class HashExtensionsTests { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/HttpContextExtensionsTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/HttpContextExtensionsTests.cs new file mode 100644 index 000000000..2b49bbd55 --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/HttpContextExtensionsTests.cs @@ -0,0 +1,35 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using AwesomeAssertions; +using Microsoft.AspNetCore.Http; +using Open.IdentityServer.Extensions; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Extensions; + +public class HttpContextExtensionsTests +{ + [Fact] + public void GetOriginalRequestPath_WhenCalledAndRequestNotRedirected_ShouldReturnRequestPath() + { + var context = new DefaultHttpContext(); + context.Request.Path = "/test"; + + var result = context.GetOriginalRequestPath(); + + result.Should().Be("/test"); + } + + [Fact] + public void GetOriginalRequestPath_WhenCalledAndRequestWasRedirected_ShouldReturnOriginalRequestPath() + { + var context = new DefaultHttpContext(); + context.Request.Path = "/test"; + context.Items[Constants.EnvironmentKeys.OriginalRequestPath] = new PathString("/original"); + + var result = context.GetOriginalRequestPath(); + + result.Should().Be("/original"); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/HttpRequestExtensionsTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/HttpRequestExtensionsTests.cs index 6c17d7a99..1099c0abe 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/HttpRequestExtensionsTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/HttpRequestExtensionsTests.cs @@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Http; using Xunit; -namespace IdentityServer.UnitTests.Extensions; +namespace Open.IdentityServer.UnitTests.Extensions; [SuppressMessage( "Usage", diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/IResourceStoreExtensionsTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/IResourceStoreExtensionsTests.cs index 8d3e6b898..11fc9a54c 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/IResourceStoreExtensionsTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/IResourceStoreExtensionsTests.cs @@ -10,7 +10,7 @@ using Open.IdentityServer.Stores; using Xunit; -namespace IdentityServer.UnitTests.Extensions; +namespace Open.IdentityServer.UnitTests.Extensions; public class IResourceStoreExtensionsTests { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/IdentityServerBuilderExtensionsCacheStoreTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/IdentityServerBuilderExtensionsCacheStoreTests.cs index e0cfaa9ba..04fa934c0 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/IdentityServerBuilderExtensionsCacheStoreTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/IdentityServerBuilderExtensionsCacheStoreTests.cs @@ -11,7 +11,7 @@ using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace IdentityServer.UnitTests.Extensions; +namespace Open.IdentityServer.UnitTests.Extensions; public class IdentityServerBuilderExtensionsCacheStoreTests { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/IdentityServerBuilderExtensionsCryptoTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/IdentityServerBuilderExtensionsCryptoTests.cs index 3f027ff5e..2f824f12e 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/IdentityServerBuilderExtensionsCryptoTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/IdentityServerBuilderExtensionsCryptoTests.cs @@ -11,7 +11,7 @@ using System.Security.Cryptography; using Xunit; -namespace IdentityServer.UnitTests.Extensions; +namespace Open.IdentityServer.UnitTests.Extensions; public class IdentityServerBuilderExtensionsCryptoTests { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/JwtPayloadCreationTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/JwtPayloadCreationTests.cs index b383f7e7c..dd53a2820 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/JwtPayloadCreationTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/JwtPayloadCreationTests.cs @@ -7,7 +7,7 @@ using System.Linq; using System.Security.Claims; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer.Configuration; using Open.IdentityServer.Extensions; using Open.IdentityServer.Models; @@ -16,7 +16,7 @@ using Open.IdentityServer; using Xunit; -namespace IdentityServer.UnitTests.Extensions; +namespace Open.IdentityServer.UnitTests.Extensions; public class JwtPayloadCreationTests { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/StringExtensionsTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/StringExtensionsTests.cs index cf708e765..c2f4b6964 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/StringExtensionsTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/StringExtensionsTests.cs @@ -5,7 +5,7 @@ using Open.IdentityServer.Extensions; using Xunit; -namespace IdentityServer.UnitTests.Extensions; +namespace Open.IdentityServer.UnitTests.Extensions; public class StringExtensionsTests { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/ValidatedAuthorizeRequestExtensionsTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/ValidatedAuthorizeRequestExtensionsTests.cs index 68201a0dc..25ba13a7a 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/ValidatedAuthorizeRequestExtensionsTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Extensions/ValidatedAuthorizeRequestExtensionsTests.cs @@ -4,7 +4,7 @@ using Open.IdentityServer.Validation; using Xunit; -namespace IdentityServer.UnitTests.Extensions; +namespace Open.IdentityServer.UnitTests.Extensions; public class ValidatedAuthorizeRequestExtensionsTests { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Hosting/EndpointRouterTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Hosting/EndpointRouterTests.cs index c34e8ee54..6c63133e8 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Hosting/EndpointRouterTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Hosting/EndpointRouterTests.cs @@ -6,14 +6,14 @@ using System.Collections.Generic; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer.Configuration; using Open.IdentityServer.Hosting; using Microsoft.AspNetCore.Http; using Xunit; using static Open.IdentityServer.Constants; -namespace IdentityServer.UnitTests.Hosting; +namespace Open.IdentityServer.UnitTests.Hosting; public class EndpointRouterTests { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Hosting/IdentityServerMiddlewareTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Hosting/IdentityServerMiddlewareTests.cs new file mode 100644 index 000000000..fd020a359 --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Hosting/IdentityServerMiddlewareTests.cs @@ -0,0 +1,211 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System; +using System.Threading.Tasks; +using AwesomeAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Moq; +using Open.IdentityServer.Hosting; +using Open.IdentityServer.Services; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Hosting; + +public class IdentityServerMiddlewareTests +{ + private readonly IdentityServerMiddleware _subject; + + private readonly RequestDelegate _next; + private bool nextCalled; + + private readonly Mock> _loggerMock; + private readonly Mock _router; + private readonly Mock _userSession; + private readonly Mock _eventService; + private readonly Mock _backChannelLogoutService; + private readonly Mock _telemetryService; + private readonly Mock _trace; + private readonly DefaultHttpContext _context; + + public IdentityServerMiddlewareTests() + { + _next = context => { + nextCalled = true; + return Task.CompletedTask; + }; + + _loggerMock = new Mock>(); + _router = new Mock(); + _userSession = new Mock(); + _eventService = new Mock(); + _backChannelLogoutService = new Mock(); + _telemetryService = new Mock(); + _trace = new Mock(); + _telemetryService.Setup(t => t.Trace(It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + _userSession.Setup(x => x.EnsureSessionIdCookieAsync()).Returns(Task.CompletedTask); + _router.Setup(x => x.Find(It.IsAny())).Returns((IEndpointHandler)null); + + _subject = new IdentityServerMiddleware(_next, _loggerMock.Object); + + _context = new DefaultHttpContext(); + } + + private async Task InvokeSubjectMiddleware() + { + await _subject.Invoke(_context, _router.Object, _userSession.Object, + _eventService.Object, _backChannelLogoutService.Object, _telemetryService?.Object); + } + + [Fact] + public async Task Invoke_WithNoMatchingEndpoint_ShouldCallNext() + { + _router.Setup(x => x.Find(_context)).Returns((IEndpointHandler)null); + + await InvokeSubjectMiddleware(); + + nextCalled.Should().BeTrue(); + } + + [Fact] + public async Task Invoke_ShouldEnsureSessionIdCookie() + { + await InvokeSubjectMiddleware(); + + _userSession.Verify(x => x.EnsureSessionIdCookieAsync(), Times.Once); + } + + [Fact] + public async Task Invoke_WhenRouterLocatesEndpoint_ShouldProcessEndpoint() + { + var endpointMock = new Mock(); + var endpointResult = new Mock(); + + endpointMock + .Setup(x => x.ProcessAsync(_context)) + .ReturnsAsync(endpointResult.Object); + + _router.Setup(x => x.Find(_context)).Returns(endpointMock.Object); + + await InvokeSubjectMiddleware(); + + endpointMock.Verify(x => x.ProcessAsync(_context), Times.Once); + nextCalled.Should().BeFalse(); + } + + [Fact] + public async Task Invoke_WhenRouterLocatesEndpoint_ShouldTrace() + { + var endpointMock = new Mock(); + var endpointResult = new Mock(); + + endpointMock + .Setup(x => x.ProcessAsync(_context)) + .ReturnsAsync(endpointResult.Object); + + _router.Setup(x => x.Find(_context)).Returns(endpointMock.Object); + + await InvokeSubjectMiddleware(); + + _telemetryService.Verify(x => x.Trace(It.IsAny(), It.IsAny()), Times.Once); + _trace.Verify(x => x.Dispose(), Times.Once); + } + + [Fact] + public async Task Invoke_WhenRouterEndpointIsNull_ShouldNotTrace() + { + _router.Setup(x => x.Find(_context)).Returns((IEndpointHandler)null); + + await InvokeSubjectMiddleware(); + + _telemetryService.Verify(x => x.Trace(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task Invoke_WhenProcessedEndpointProducesResult_ShouldExecuteResult() + { + var endpointMock = new Mock(); + var resultMock = new Mock(); + + endpointMock + .Setup(x => x.ProcessAsync(_context)) + .ReturnsAsync(resultMock.Object); + + _router.Setup(x => x.Find(_context)).Returns(endpointMock.Object); + + await InvokeSubjectMiddleware(); + + resultMock.Verify(x => x.ExecuteAsync(_context), Times.Once); + nextCalled.Should().BeFalse(); + } + + [Fact] + public async Task Invoke_OnUnhandledException_ShouldCountTelemetryEvent() + { + _router.Setup(x => x.Find(_context)).Throws(new Exception("Test exception")); + + try + { + await InvokeSubjectMiddleware(); + } + catch (Exception e) + { + // intentionally swallowed + } + + _telemetryService.Verify(x => x.CountInternalError("System.Exception"), Times.Once); + } + + [Fact] + public async Task Invoke_OnUnhandledException_ShouldAttachToTrace() + { + var exception = new Exception("Test exception"); + var endpointMock = new Mock(); + + endpointMock + .Setup(x => x.ProcessAsync(_context)) + .ThrowsAsync(exception); + + _router.Setup(x => x.Find(_context)).Returns(endpointMock.Object); + + try + { + await InvokeSubjectMiddleware(); + } + catch (Exception e) + { + // intentionally swallowed + } + + _trace.Verify(t => t.AddException(exception), Times.Once); + } + + [Fact] + public async Task Invoke_WhenRouterLocatesEndpoint_ShouldTrackActiveRequest() + { + var endpointMock = new Mock(); + var endpointResult = new Mock(); + + endpointMock + .Setup(x => x.ProcessAsync(_context)) + .ReturnsAsync(endpointResult.Object); + + _router.Setup(x => x.Find(_context)).Returns(endpointMock.Object); + + var activeRequestDisposable = new Mock(); + using var telemetryScope = activeRequestDisposable.Object; + _telemetryService.Setup(x => x.BeginActiveRequest(It.IsAny(), It.IsAny())) + .Returns(telemetryScope); + + await InvokeSubjectMiddleware(); + + _telemetryService.Verify(x => x.BeginActiveRequest( + endpointMock.Object.GetType().FullName, + _context.Request.Path.ToString()), + Times.Once); + activeRequestDisposable.Verify(x => x.Dispose(), Times.Once); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/IdentityServerToolsTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/IdentityServerToolsTests.cs index 10c8832f4..e1cb7e2b9 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/IdentityServerToolsTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/IdentityServerToolsTests.cs @@ -3,7 +3,7 @@ using System.Linq; using System.Security.Claims; using System.Threading.Tasks; -using IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Common; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Time.Testing; using Moq; diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Infrastructure/ObjectSerializerTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Infrastructure/ObjectSerializerTests.cs index 2f672b2ef..466f719af 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Infrastructure/ObjectSerializerTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Infrastructure/ObjectSerializerTests.cs @@ -6,7 +6,7 @@ using Open.IdentityServer.Models; using Xunit; -namespace IdentityServer.UnitTests.Infrastructure; +namespace Open.IdentityServer.UnitTests.Infrastructure; public class ObjectSerializerTests { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Open.IdentityServer.UnitTests.csproj b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Open.IdentityServer.UnitTests.csproj index bb82a0c73..46644c63d 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Open.IdentityServer.UnitTests.csproj +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Open.IdentityServer.UnitTests.csproj @@ -17,6 +17,7 @@ + diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeInteractionResponseGenerator/AuthorizeInteractionResponseGeneratorTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeInteractionResponseGenerator/AuthorizeInteractionResponseGeneratorTests.cs index 49f3fd068..29ec9357e 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeInteractionResponseGenerator/AuthorizeInteractionResponseGeneratorTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeInteractionResponseGenerator/AuthorizeInteractionResponseGeneratorTests.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -6,14 +7,16 @@ using System.Collections.Generic; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; +using Moq; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer; using Open.IdentityServer.Configuration; using Open.IdentityServer.Models; +using Open.IdentityServer.Services; using Open.IdentityServer.Validation; using Xunit; -namespace IdentityServer.UnitTests.ResponseHandling.AuthorizeInteractionResponseGenerator; +namespace Open.IdentityServer.UnitTests.ResponseHandling.AuthorizeInteractionResponseGenerator; public class AuthorizeInteractionResponseGeneratorTests { @@ -21,6 +24,8 @@ public class AuthorizeInteractionResponseGeneratorTests private readonly Open.IdentityServer.ResponseHandling.AuthorizeInteractionResponseGenerator _subject; private readonly MockConsentService _mockConsentService = new MockConsentService(); private readonly StubClock _clock = new StubClock(); + private readonly Mock _telemetry = new(); + private readonly Mock _trace = new (); public AuthorizeInteractionResponseGeneratorTests() { @@ -28,10 +33,10 @@ public AuthorizeInteractionResponseGeneratorTests() _clock, TestLogger.Create(), _mockConsentService, - new MockProfileService()); + new MockProfileService(), + _telemetry.Object); } - [Fact] public async Task Authenticated_User_with_restricted_current_Idp_with_prompt_none_must_error() { @@ -155,4 +160,33 @@ public async Task locally_authenticated_user_but_client_does_not_allow_local_wit result.IsError.Should().BeTrue(); result.IsLogin.Should().BeFalse(); } + + [Fact] + public async Task process_interaction_should_initiate_telemetry_trace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var request = new ValidatedAuthorizeRequest + { + ClientId = "foo", + Client = new Client() + { + EnableLocalLogin = false + }, + Subject = new IdentityServerUser("123") + { + IdentityProvider = IdentityServerConstants.LocalIdentityProvider + }.CreatePrincipal(), + PromptModes = new[] { OidcConstants.PromptModes.None } + }; + + await _subject.ProcessInteractionAsync(request); + + _telemetry.Verify( + t => t.Trace(TelemetryConstants.TraceCategories.Basic, + _subject, + "ProcessInteractionAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } } \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeInteractionResponseGenerator/AuthorizeInteractionResponseGeneratorTests_Consent.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeInteractionResponseGenerator/AuthorizeInteractionResponseGeneratorTests_Consent.cs index 2fd9be42c..0e7289378 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeInteractionResponseGenerator/AuthorizeInteractionResponseGeneratorTests_Consent.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeInteractionResponseGenerator/AuthorizeInteractionResponseGeneratorTests_Consent.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -8,20 +9,23 @@ using System.Security.Claims; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; +using Moq; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer; using Open.IdentityServer.Models; +using Open.IdentityServer.Services; using Open.IdentityServer.Utility; using Open.IdentityServer.Validation; using Xunit; -namespace IdentityServer.UnitTests.ResponseHandling.AuthorizeInteractionResponseGenerator; +namespace Open.IdentityServer.UnitTests.ResponseHandling.AuthorizeInteractionResponseGenerator; public class AuthorizeInteractionResponseGeneratorTests_Consent { private readonly Open.IdentityServer.ResponseHandling.AuthorizeInteractionResponseGenerator _subject; private readonly MockConsentService _mockConsent = new(); private readonly MockProfileService _fakeUserService = new(); + private readonly Mock _mockTelemetryService = new (); private void RequiresConsent(bool value) { @@ -95,7 +99,8 @@ public AuthorizeInteractionResponseGeneratorTests_Consent() new StubClock(), TestLogger.Create(), _mockConsent, - _fakeUserService); + _fakeUserService, + _mockTelemetryService.Object); } private static ResourceValidationResult GetValidatedResources(params string[] scopes) diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeInteractionResponseGenerator/AuthorizeInteractionResponseGeneratorTests_Custom.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeInteractionResponseGenerator/AuthorizeInteractionResponseGeneratorTests_Custom.cs index 5bb3bb065..41421ecdc 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeInteractionResponseGenerator/AuthorizeInteractionResponseGeneratorTests_Custom.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeInteractionResponseGenerator/AuthorizeInteractionResponseGeneratorTests_Custom.cs @@ -1,11 +1,12 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. using System; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer; using Open.IdentityServer.Configuration; using Open.IdentityServer.Models; @@ -13,16 +14,17 @@ using Open.IdentityServer.Services; using Open.IdentityServer.Validation; using Microsoft.Extensions.Logging; +using Moq; using Xunit; -namespace IdentityServer.UnitTests.ResponseHandling.AuthorizeInteractionResponseGenerator; +namespace Open.IdentityServer.UnitTests.ResponseHandling.AuthorizeInteractionResponseGenerator; public class CustomAuthorizeInteractionResponseGenerator : Open.IdentityServer.ResponseHandling.AuthorizeInteractionResponseGenerator { public CustomAuthorizeInteractionResponseGenerator( TimeProvider clock, ILogger logger, - IConsentService consent, IProfileService profile) : base(clock, logger, consent, profile) + IConsentService consent, IProfileService profile, ITelemetryService telemetry) : base(clock, logger, consent, profile, telemetry) { } @@ -54,6 +56,7 @@ public class AuthorizeInteractionResponseGeneratorTests_Custom private readonly CustomAuthorizeInteractionResponseGenerator _subject; private readonly MockConsentService _mockConsentService = new MockConsentService(); private readonly StubClock _clock = new StubClock(); + private readonly Mock _mockTelemetryService = new Mock(); public AuthorizeInteractionResponseGeneratorTests_Custom() { @@ -61,7 +64,8 @@ public AuthorizeInteractionResponseGeneratorTests_Custom() _clock, TestLogger.Create(), _mockConsentService, - new MockProfileService()); + new MockProfileService(), + _mockTelemetryService.Object); } diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeInteractionResponseGenerator/AuthorizeInteractionResponseGeneratorTests_Login.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeInteractionResponseGenerator/AuthorizeInteractionResponseGeneratorTests_Login.cs index eb3482492..8a9dbdb93 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeInteractionResponseGenerator/AuthorizeInteractionResponseGeneratorTests_Login.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeInteractionResponseGenerator/AuthorizeInteractionResponseGeneratorTests_Login.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -7,15 +8,17 @@ using System.Collections.Specialized; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; +using Moq; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer; using Open.IdentityServer.Configuration; using Open.IdentityServer.Models; +using Open.IdentityServer.Services; using Open.IdentityServer.Utility; using Open.IdentityServer.Validation; using Xunit; -namespace IdentityServer.UnitTests.ResponseHandling.AuthorizeInteractionResponseGenerator; +namespace Open.IdentityServer.UnitTests.ResponseHandling.AuthorizeInteractionResponseGenerator; public class AuthorizeInteractionResponseGeneratorTests_Login { @@ -23,6 +26,7 @@ public class AuthorizeInteractionResponseGeneratorTests_Login private readonly Open.IdentityServer.ResponseHandling.AuthorizeInteractionResponseGenerator _subject; private readonly MockConsentService _mockConsentService = new MockConsentService(); private readonly StubClock _clock = new StubClock(); + private readonly Mock _telemetry = new Mock(); public AuthorizeInteractionResponseGeneratorTests_Login() { @@ -30,7 +34,8 @@ public AuthorizeInteractionResponseGeneratorTests_Login() _clock, TestLogger.Create(), _mockConsentService, - new MockProfileService()); + new MockProfileService(), + _telemetry.Object); } [Fact] diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeResponseGenerator/AuthorizeResponseGeneratorTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeResponseGenerator/AuthorizeResponseGeneratorTests.cs index 0e7f248a1..851456f54 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeResponseGenerator/AuthorizeResponseGeneratorTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeResponseGenerator/AuthorizeResponseGeneratorTests.cs @@ -1,14 +1,18 @@ // Copyright (c) 2026, Rock Solid Knowledge Ltd // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. +using System; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Time.Testing; using Moq; using Open.IdentityServer.Services; using Open.IdentityServer.Stores; +using Open.IdentityServer.Validation; +using Xunit; -namespace IdentityServer.UnitTests.ResponseHandling.AuthorizeResponseGenerator; +namespace Open.IdentityServer.UnitTests.ResponseHandling.AuthorizeResponseGenerator; public abstract class AuthorizeResponseGeneratorTests { @@ -18,7 +22,9 @@ public abstract class AuthorizeResponseGeneratorTests protected IAuthorizationCodeStore authorizationCodeStore = Mock.Of(); protected ILogger logger = NullLogger.Instance; protected IEventService events = Mock.Of(); + protected ITelemetryService telemetry = Mock.Of(); + protected ITrace trace = Mock.Of(); protected Open.IdentityServer.ResponseHandling.AuthorizeResponseGenerator CreateSut() => - new(clock, tokenService, keyMaterialService, authorizationCodeStore, logger, events); + new(clock, tokenService, keyMaterialService, authorizationCodeStore, logger, events, telemetry); } \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeResponseGenerator/AuthorizeResponseGeneratorTests_Code.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeResponseGenerator/AuthorizeResponseGeneratorTests_Code.cs index fe11efa75..31c0ac3d6 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeResponseGenerator/AuthorizeResponseGeneratorTests_Code.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeResponseGenerator/AuthorizeResponseGeneratorTests_Code.cs @@ -11,10 +11,30 @@ using Open.IdentityServer.Validation; using Xunit; -namespace IdentityServer.UnitTests.ResponseHandling.AuthorizeResponseGenerator; +namespace Open.IdentityServer.UnitTests.ResponseHandling.AuthorizeResponseGenerator; public class AuthorizeResponseGeneratorTests_Code : AuthorizeResponseGeneratorTests { + [Fact] + public async Task create_response_should_initiate_telemetry_trace() + { + Mock.Get(telemetry) + .Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(trace); + + var request = CreateValidatedAuthorizeRequest(GrantType.AuthorizationCode); + var subject = CreateSut(); + + await subject.CreateResponseAsync(request); + + Mock.Get(telemetry) + .Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Basic, + subject, "CreateResponseAsync"), Times.Once); + Mock.Get(trace) + .Verify(t => t.Dispose(), Times.Once); + } + [Fact] public async Task CreateResponseAsync_CodeFlow_ReturnsCodeResponse() { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeResponseGenerator/AuthorizeResponseGeneratorTests_Hybrid.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeResponseGenerator/AuthorizeResponseGeneratorTests_Hybrid.cs index 7bc6b2632..772f2a9d8 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeResponseGenerator/AuthorizeResponseGeneratorTests_Hybrid.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeResponseGenerator/AuthorizeResponseGeneratorTests_Hybrid.cs @@ -11,7 +11,7 @@ using Open.IdentityServer.Validation; using Xunit; -namespace IdentityServer.UnitTests.ResponseHandling.AuthorizeResponseGenerator; +namespace Open.IdentityServer.UnitTests.ResponseHandling.AuthorizeResponseGenerator; public class AuthorizeResponseGeneratorTests_Hybrid : AuthorizeResponseGeneratorTests { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeResponseGenerator/AuthorizeResponseGeneratorTests_Implicit.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeResponseGenerator/AuthorizeResponseGeneratorTests_Implicit.cs index 1cc8fd697..040866953 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeResponseGenerator/AuthorizeResponseGeneratorTests_Implicit.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/AuthorizeResponseGenerator/AuthorizeResponseGeneratorTests_Implicit.cs @@ -11,7 +11,7 @@ using Open.IdentityServer.Validation; using Xunit; -namespace IdentityServer.UnitTests.ResponseHandling.AuthorizeResponseGenerator; +namespace Open.IdentityServer.UnitTests.ResponseHandling.AuthorizeResponseGenerator; public class AuthorizeResponseGeneratorTests_Implicit : AuthorizeResponseGeneratorTests { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/Default/DeviceAuthorizationResponseGeneratorTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/Default/DeviceAuthorizationResponseGeneratorTests.cs new file mode 100644 index 000000000..ff822a4a8 --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/Default/DeviceAuthorizationResponseGeneratorTests.cs @@ -0,0 +1,231 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AwesomeAssertions; +using Open.IdentityServer.UnitTests.Common; +using Open.IdentityServer.Configuration; +using Open.IdentityServer.Models; +using Open.IdentityServer.ResponseHandling; +using Open.IdentityServer.Services; +using Open.IdentityServer.Services.Default; +using Open.IdentityServer.Stores; +using Open.IdentityServer.Validation; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Open.IdentityServer.UnitTests.Validation.Setup; +using Xunit; + +namespace Open.IdentityServer.UnitTests.ResponseHandling; + +public class DeviceAuthorizationResponseGeneratorTests +{ + private readonly List _identityResources = + [new IdentityResources.OpenId(), new IdentityResources.Profile()]; + private readonly List _apiResources = [new("resource") { Scopes = { "api1" } }]; + private readonly List _scopes = [new("api1")]; + + private readonly FakeUserCodeGenerator _fakeUserCodeGenerator = new(); + private readonly IDeviceFlowCodeService _deviceFlowCodeService = new DefaultDeviceFlowCodeService(new InMemoryDeviceFlowStore(), new StubHandleGenerationService(), new NopTelemetryService()); + private readonly IdentityServerOptions _options = new(); + private readonly StubClock _clock = new(); + private readonly Mock _telemetry = new(); + private readonly Mock _trace = new(); + + private readonly DeviceAuthorizationResponseGenerator _generator; + private readonly DeviceAuthorizationRequestValidationResult _testResult; + private const string TestBaseUrl = "http://localhost:5000/"; + + public DeviceAuthorizationResponseGeneratorTests() + { + _testResult = new DeviceAuthorizationRequestValidationResult(new ValidatedDeviceAuthorizationRequest + { + Client = new Client {ClientId = Guid.NewGuid().ToString()}, + IsOpenIdRequest = true, + ValidatedResources = new ResourceValidationResult() + }); + + _generator = new DeviceAuthorizationResponseGenerator( + _options, + new DefaultUserCodeService([new NumericUserCodeGenerator(), _fakeUserCodeGenerator]), + _deviceFlowCodeService, + _clock, + _telemetry.Object, + new NullLogger()); + } + + + [Fact] + public async Task ProcessAsync_WhenCalled_ShouldTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var creationTime = DateTime.UtcNow; + _clock.UtcNowFunc = () => creationTime; + + _testResult.ValidatedRequest.Client.UserCodeType = FakeUserCodeGenerator.UserCodeTypeValue; + await _deviceFlowCodeService.StoreDeviceAuthorizationAsync(FakeUserCodeGenerator.TestCollisionUserCode, new DeviceCode()); + + await _generator.ProcessAsync(_testResult, TestBaseUrl); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Basic, _generator, "ProcessAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task ProcessAsync_when_valiationresult_null_exect_exception() + { + Func act = () => _generator.ProcessAsync(null, TestBaseUrl); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task ProcessAsync_when_valiationresult_client_null_exect_exception() + { + var validationResult = new DeviceAuthorizationRequestValidationResult(new ValidatedDeviceAuthorizationRequest()); + Func act = () => _generator.ProcessAsync(validationResult, TestBaseUrl); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task ProcessAsync_when_baseurl_null_exect_exception() + { + Func act = () => _generator.ProcessAsync(_testResult, null); + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task ProcessAsync_when_user_code_collision_expect_retry() + { + var creationTime = DateTime.UtcNow; + _clock.UtcNowFunc = () => creationTime; + + _testResult.ValidatedRequest.Client.UserCodeType = FakeUserCodeGenerator.UserCodeTypeValue; + await _deviceFlowCodeService.StoreDeviceAuthorizationAsync(FakeUserCodeGenerator.TestCollisionUserCode, new DeviceCode()); + + var response = await _generator.ProcessAsync(_testResult, TestBaseUrl); + + response.UserCode.Should().Be(FakeUserCodeGenerator.TestUniqueUserCode); + } + + [Fact] + public async Task ProcessAsync_when_user_code_collision_retry_limit_reached_expect_error() + { + var creationTime = DateTime.UtcNow; + _clock.UtcNowFunc = () => creationTime; + + _fakeUserCodeGenerator.RetryLimit = 1; + _testResult.ValidatedRequest.Client.UserCodeType = FakeUserCodeGenerator.UserCodeTypeValue; + await _deviceFlowCodeService.StoreDeviceAuthorizationAsync(FakeUserCodeGenerator.TestCollisionUserCode, new DeviceCode()); + + await Assert.ThrowsAsync(() => _generator.ProcessAsync(_testResult, TestBaseUrl)); + } + + [Fact] + public async Task ProcessAsync_when_generated_expect_user_code_stored() + { + var creationTime = DateTime.UtcNow; + _clock.UtcNowFunc = () => creationTime; + + _testResult.ValidatedRequest.RequestedScopes = new List { "openid", "api1" }; + _testResult.ValidatedRequest.ValidatedResources = new ResourceValidationResult(new Resources( + _identityResources.Where(x=>x.Name == "openid"), + _apiResources.Where(x=>x.Name == "resource"), + _scopes.Where(x=>x.Name == "api1"))); + + var response = await _generator.ProcessAsync(_testResult, TestBaseUrl); + + response.UserCode.Should().NotBeNullOrWhiteSpace(); + + var userCode = await _deviceFlowCodeService.FindByUserCodeAsync(response.UserCode); + userCode.Should().NotBeNull(); + userCode.ClientId.Should().Be(_testResult.ValidatedRequest.Client.ClientId); + userCode.Lifetime.Should().Be(_testResult.ValidatedRequest.Client.DeviceCodeLifetime); + userCode.CreationTime.Should().Be(creationTime); + userCode.Subject.Should().BeNull(); + userCode.AuthorizedScopes.Should().BeNull(); + + userCode.RequestedScopes.Should().Contain(_testResult.ValidatedRequest.RequestedScopes); + } + + [Fact] + public async Task ProcessAsync_when_generated_expect_device_code_stored() + { + var creationTime = DateTime.UtcNow; + _clock.UtcNowFunc = () => creationTime; + + var response = await _generator.ProcessAsync(_testResult, TestBaseUrl); + + response.DeviceCode.Should().NotBeNullOrWhiteSpace(); + response.Interval.Should().Be(_options.DeviceFlow.Interval); + + var deviceCode = await _deviceFlowCodeService.FindByDeviceCodeAsync(response.DeviceCode); + deviceCode.Should().NotBeNull(); + deviceCode.ClientId.Should().Be(_testResult.ValidatedRequest.Client.ClientId); + deviceCode.IsOpenId.Should().Be(_testResult.ValidatedRequest.IsOpenIdRequest); + deviceCode.Lifetime.Should().Be(_testResult.ValidatedRequest.Client.DeviceCodeLifetime); + deviceCode.CreationTime.Should().Be(creationTime); + deviceCode.Subject.Should().BeNull(); + deviceCode.AuthorizedScopes.Should().BeNull(); + + response.DeviceCodeLifetime.Should().Be(deviceCode.Lifetime); + } + + [Fact] + public async Task ProcessAsync_when_DeviceVerificationUrl_is_relative_uri_expect_correct_VerificationUris() + { + const string baseUrl = "http://localhost:5000/"; + _options.UserInteraction.DeviceVerificationUrl = "/device"; + _options.UserInteraction.DeviceVerificationUserCodeParameter = "userCode"; + + var response = await _generator.ProcessAsync(_testResult, baseUrl); + + response.VerificationUri.Should().Be("http://localhost:5000/device"); + response.VerificationUriComplete.Should().StartWith("http://localhost:5000/device?userCode="); + } + + [Fact] + public async Task ProcessAsync_when_DeviceVerificationUrl_is_absolute_uri_expect_correct_VerificationUris() + { + const string baseUrl = "http://localhost:5000/"; + _options.UserInteraction.DeviceVerificationUrl = "http://short/device"; + _options.UserInteraction.DeviceVerificationUserCodeParameter = "userCode"; + + var response = await _generator.ProcessAsync(_testResult, baseUrl); + + response.VerificationUri.Should().Be("http://short/device"); + response.VerificationUriComplete.Should().StartWith("http://short/device?userCode="); + } +} + +internal class FakeUserCodeGenerator : IUserCodeGenerator +{ + public const string UserCodeTypeValue = "Collider"; + public const string TestUniqueUserCode = "123"; + public const string TestCollisionUserCode = "321"; + private int tryCount; + + + public string UserCodeType => UserCodeTypeValue; + + public int RetryLimit { get; set; } = 2; + + public Task GenerateAsync() + { + if (tryCount == 0) + { + tryCount++; + return Task.FromResult(TestCollisionUserCode); + } + + tryCount++; + return Task.FromResult(TestUniqueUserCode); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/Default/DiscoveryResponseGeneratorTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/Default/DiscoveryResponseGeneratorTests.cs index 6dadda9c5..3d0a57de3 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/Default/DiscoveryResponseGeneratorTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/Default/DiscoveryResponseGeneratorTests.cs @@ -17,38 +17,86 @@ using Open.IdentityServer.Validation; using Xunit; -namespace IdentityServer.UnitTests.ResponseHandling.Default; +namespace Open.IdentityServer.UnitTests.ResponseHandling.Default; public class DiscoveryResponseGeneratorTests { //ExtensionGrantValidator Mocks - private readonly IEnumerable ExtensionGrantValidators = []; - private readonly ILogger ExtensionGrantValidatorLogger = Mock.Of>(); - - private readonly IdentityServerOptions Options = new(); - private ExtensionGrantValidator ExtensionGrants; - private readonly IKeyMaterialService Keys = Mock.Of(); - private readonly IResourceOwnerPasswordValidator ResourceOwnerValidator = Mock.Of(); - private readonly IResourceStore ResourceStore = Mock.Of(); - private readonly ISecretsListParser SecretParsers = Mock.Of(); - private readonly ILogger Logger = NullLogger.Instance; + private readonly IEnumerable _extensionGrantValidators = []; + private readonly ILogger _extensionGrantValidatorLogger = Mock.Of>(); + + private readonly IdentityServerOptions _options = new(); + private ExtensionGrantValidator _extensionGrants; + private readonly IKeyMaterialService _keys = Mock.Of(); + private readonly IResourceOwnerPasswordValidator _resourceOwnerValidator = Mock.Of(); + private readonly IResourceStore _resourceStore = Mock.Of(); + private readonly ISecretsListParser _secretParsers = Mock.Of(); + private readonly ITelemetryService _telemetry = Mock.Of(); + private readonly ITrace _trace = Mock.Of(); + private readonly ILogger _logger = NullLogger.Instance; private DiscoveryResponseGenerator CreateSut() { - ExtensionGrants = new ExtensionGrantValidator(ExtensionGrantValidators, ExtensionGrantValidatorLogger); + _extensionGrants = new ExtensionGrantValidator(_extensionGrantValidators, _extensionGrantValidatorLogger); - Mock.Get(ResourceStore) + Mock.Get(_resourceStore) .Setup(x => x.GetAllResourcesAsync()) .ReturnsAsync(new Resources()); - return new DiscoveryResponseGenerator(Options, ResourceStore, Keys, ExtensionGrants, SecretParsers, - ResourceOwnerValidator, Logger); + return new DiscoveryResponseGenerator( + _options, + _resourceStore, + _keys, + _extensionGrants, + _secretParsers, + _resourceOwnerValidator, + _telemetry, + _logger); + } + + [Fact] + public async Task CreateDiscoveryDocumentAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + Mock.Get(_telemetry).Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace); + + var sut = CreateSut(); + + await sut.CreateDiscoveryDocumentAsync("https://open.ids.url/somepath", "https://open.ids.url"); + + Mock.Get(_telemetry) + .Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Basic, + sut, + "CreateDiscoveryDocumentAsync")); + Mock.Get(_trace).Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task CreateJwkDocumentASync_WhenCalled_ShouldInitiateTelemetryTrace() + { + Mock.Get(_telemetry).Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace); + Mock.Get(_keys) + .Setup(k => k.GetValidationKeysAsync()) + .ReturnsAsync([]); + + var sut = CreateSut(); + + await sut.CreateJwkDocumentAsync(); + + Mock.Get(_telemetry) + .Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Basic, + sut, + "CreateJwkDocumentAsync")); + Mock.Get(_trace).Verify(t => t.Dispose(), Times.Once); } [Fact] public async Task CreateDiscoveryDocumentAsync_WhenAuthoriseEndpointDisabled_ShouldContainAuthorizationResponseIssParameterSupportedAsTrue() { - Options.Endpoints.EnableAuthorizeEndpoint = false; + _options.Endpoints.EnableAuthorizeEndpoint = false; var sut = CreateSut(); @@ -61,7 +109,7 @@ public async Task CreateDiscoveryDocumentAsync_WhenAuthoriseEndpointDisabled_Sho [InlineData(true), InlineData(false)] public async Task CreateDiscoveryDocumentAsync_WhenAuthoriseEndpointEnabled_ShouldContainAuthorizationResponseIssParameterSupported(bool value) { - Options.EnableAuthorizeResponseIssuerParam = value; + _options.EnableAuthorizeResponseIssuerParam = value; var sut = CreateSut(); @@ -86,7 +134,7 @@ public async Task CreateDiscoveryDocumentAsync_WhenCalled_ShouldContainIssuer() [Fact] public async Task CreateDiscoveryDocumentAsync_WhenAuthorizeEndpointEnabled_ShouldContainAuthorizationEndpoint() { - Options.Endpoints.EnableAuthorizeEndpoint = true; + _options.Endpoints.EnableAuthorizeEndpoint = true; var sut = CreateSut(); @@ -98,7 +146,7 @@ public async Task CreateDiscoveryDocumentAsync_WhenAuthorizeEndpointEnabled_Shou [Fact] public async Task CreateDiscoveryDocumentAsync_WhenAuthorizeEndpointDisabled_ShouldNotContainAuthorizationEndpoint() { - Options.Endpoints.EnableAuthorizeEndpoint = false; + _options.Endpoints.EnableAuthorizeEndpoint = false; var sut = CreateSut(); @@ -110,7 +158,7 @@ public async Task CreateDiscoveryDocumentAsync_WhenAuthorizeEndpointDisabled_Sho [Fact] public async Task CreateDiscoveryDocumentAsync_WhenTokenEndpointEnabled_ShouldContainTokenEndpoint() { - Options.Endpoints.EnableTokenEndpoint = true; + _options.Endpoints.EnableTokenEndpoint = true; var sut = CreateSut(); @@ -122,7 +170,7 @@ public async Task CreateDiscoveryDocumentAsync_WhenTokenEndpointEnabled_ShouldCo [Fact] public async Task CreateDiscoveryDocumentAsync_WhenTokenEndpointDisabled_ShouldNotContainTokenEndpoint() { - Options.Endpoints.EnableTokenEndpoint = false; + _options.Endpoints.EnableTokenEndpoint = false; var sut = CreateSut(); @@ -134,7 +182,7 @@ public async Task CreateDiscoveryDocumentAsync_WhenTokenEndpointDisabled_ShouldN [Fact] public async Task CreateDiscoveryDocumentAsync_WhenUserInfoEndpointEnabled_ShouldContainUserInfoEndpoint() { - Options.Endpoints.EnableUserInfoEndpoint = true; + _options.Endpoints.EnableUserInfoEndpoint = true; var sut = CreateSut(); @@ -146,7 +194,7 @@ public async Task CreateDiscoveryDocumentAsync_WhenUserInfoEndpointEnabled_Shoul [Fact] public async Task CreateDiscoveryDocumentAsync_WhenUserInfoEndpointDisabled_ShouldNotContainUserInfoEndpoint() { - Options.Endpoints.EnableUserInfoEndpoint = false; + _options.Endpoints.EnableUserInfoEndpoint = false; var sut = CreateSut(); @@ -158,7 +206,7 @@ public async Task CreateDiscoveryDocumentAsync_WhenUserInfoEndpointDisabled_Shou [Fact] public async Task CreateDiscoveryDocumentAsync_WhenEndSessionEndpointEnabled_ShouldContainEndSessionEndpoint() { - Options.Endpoints.EnableEndSessionEndpoint = true; + _options.Endpoints.EnableEndSessionEndpoint = true; var sut = CreateSut(); @@ -170,7 +218,7 @@ public async Task CreateDiscoveryDocumentAsync_WhenEndSessionEndpointEnabled_Sho [Fact] public async Task CreateDiscoveryDocumentAsync_WhenEndSessionEndpointDisabled_ShouldNotContainEndSessionEndpoint() { - Options.Endpoints.EnableEndSessionEndpoint = false; + _options.Endpoints.EnableEndSessionEndpoint = false; var sut = CreateSut(); @@ -182,7 +230,7 @@ public async Task CreateDiscoveryDocumentAsync_WhenEndSessionEndpointDisabled_Sh [Fact] public async Task CreateDiscoveryDocumentAsync_WhenEndSessionEndpointEnabled_ShouldContainLogoutSupport() { - Options.Endpoints.EnableEndSessionEndpoint = true; + _options.Endpoints.EnableEndSessionEndpoint = true; var sut = CreateSut(); @@ -201,7 +249,7 @@ public async Task CreateDiscoveryDocumentAsync_WhenEndSessionEndpointEnabled_Sho [Fact] public async Task CreateDiscoveryDocumentAsync_WhenEndSessionEndpointDisabled_ShouldNotContainLogoutSupport() { - Options.Endpoints.EnableEndSessionEndpoint = false; + _options.Endpoints.EnableEndSessionEndpoint = false; var sut = CreateSut(); @@ -214,7 +262,7 @@ public async Task CreateDiscoveryDocumentAsync_WhenEndSessionEndpointDisabled_Sh [Fact] public async Task CreateDiscoveryDocumentAsync_WhenCheckSessionEndpointEnabled_ShouldContainCheckSessionIframe() { - Options.Endpoints.EnableCheckSessionEndpoint = true; + _options.Endpoints.EnableCheckSessionEndpoint = true; var sut = CreateSut(); @@ -226,7 +274,7 @@ public async Task CreateDiscoveryDocumentAsync_WhenCheckSessionEndpointEnabled_S [Fact] public async Task CreateDiscoveryDocumentAsync_WhenCheckSessionEndpointDisabled_ShouldNotContainCheckSessionIframe() { - Options.Endpoints.EnableCheckSessionEndpoint = false; + _options.Endpoints.EnableCheckSessionEndpoint = false; var sut = CreateSut(); @@ -238,7 +286,7 @@ public async Task CreateDiscoveryDocumentAsync_WhenCheckSessionEndpointDisabled_ [Fact] public async Task CreateDiscoveryDocumentAsync_WhenRevocationEndpointEnabled_ShouldContainRevocationEndpoint() { - Options.Endpoints.EnableTokenRevocationEndpoint = true; + _options.Endpoints.EnableTokenRevocationEndpoint = true; var sut = CreateSut(); @@ -250,7 +298,7 @@ public async Task CreateDiscoveryDocumentAsync_WhenRevocationEndpointEnabled_Sho [Fact] public async Task CreateDiscoveryDocumentAsync_WhenIntrospectionEndpointEnabled_ShouldContainIntrospectionEndpoint() { - Options.Endpoints.EnableIntrospectionEndpoint = true; + _options.Endpoints.EnableIntrospectionEndpoint = true; var sut = CreateSut(); @@ -262,7 +310,7 @@ public async Task CreateDiscoveryDocumentAsync_WhenIntrospectionEndpointEnabled_ [Fact] public async Task CreateDiscoveryDocumentAsync_WhenDeviceAuthorizationEndpointEnabled_ShouldContainDeviceAuthorizationEndpoint() { - Options.Endpoints.EnableDeviceAuthorizationEndpoint = true; + _options.Endpoints.EnableDeviceAuthorizationEndpoint = true; var sut = CreateSut(); @@ -274,7 +322,7 @@ public async Task CreateDiscoveryDocumentAsync_WhenDeviceAuthorizationEndpointEn [Fact] public async Task CreateDiscoveryDocumentAsync_WhenShowEndpointsDisabled_ShouldNotContainAnyEndpoints() { - Options.Discovery.ShowEndpoints = false; + _options.Discovery.ShowEndpoints = false; var sut = CreateSut(); @@ -288,7 +336,7 @@ public async Task CreateDiscoveryDocumentAsync_WhenShowEndpointsDisabled_ShouldN [Fact] public async Task CreateDiscoveryDocumentAsync_WhenShowGrantTypesEnabled_ShouldContainStandardGrantTypes() { - Options.Discovery.ShowGrantTypes = true; + _options.Discovery.ShowGrantTypes = true; var sut = CreateSut(); @@ -305,7 +353,7 @@ public async Task CreateDiscoveryDocumentAsync_WhenShowGrantTypesEnabled_ShouldC [Fact] public async Task CreateDiscoveryDocumentAsync_WhenShowGrantTypesDisabled_ShouldNotContainGrantTypes() { - Options.Discovery.ShowGrantTypes = false; + _options.Discovery.ShowGrantTypes = false; var sut = CreateSut(); @@ -317,8 +365,8 @@ public async Task CreateDiscoveryDocumentAsync_WhenShowGrantTypesDisabled_Should [Fact] public async Task CreateDiscoveryDocumentAsync_WhenDeviceEndpointEnabled_ShouldContainDeviceCodeGrantType() { - Options.Discovery.ShowGrantTypes = true; - Options.Endpoints.EnableDeviceAuthorizationEndpoint = true; + _options.Discovery.ShowGrantTypes = true; + _options.Endpoints.EnableDeviceAuthorizationEndpoint = true; var sut = CreateSut(); @@ -332,7 +380,7 @@ public async Task CreateDiscoveryDocumentAsync_WhenDeviceEndpointEnabled_ShouldC [Fact] public async Task CreateDiscoveryDocumentAsync_WhenShowResponseTypesEnabled_ShouldContainResponseTypes() { - Options.Discovery.ShowResponseTypes = true; + _options.Discovery.ShowResponseTypes = true; var sut = CreateSut(); @@ -344,7 +392,7 @@ public async Task CreateDiscoveryDocumentAsync_WhenShowResponseTypesEnabled_Shou [Fact] public async Task CreateDiscoveryDocumentAsync_WhenShowResponseTypesDisabled_ShouldNotContainResponseTypes() { - Options.Discovery.ShowResponseTypes = false; + _options.Discovery.ShowResponseTypes = false; var sut = CreateSut(); @@ -356,7 +404,7 @@ public async Task CreateDiscoveryDocumentAsync_WhenShowResponseTypesDisabled_Sho [Fact] public async Task CreateDiscoveryDocumentAsync_WhenShowResponseModesEnabled_ShouldContainResponseModes() { - Options.Discovery.ShowResponseModes = true; + _options.Discovery.ShowResponseModes = true; var sut = CreateSut(); @@ -368,7 +416,7 @@ public async Task CreateDiscoveryDocumentAsync_WhenShowResponseModesEnabled_Shou [Fact] public async Task CreateDiscoveryDocumentAsync_WhenShowResponseModesDisabled_ShouldNotContainResponseModes() { - Options.Discovery.ShowResponseModes = false; + _options.Discovery.ShowResponseModes = false; var sut = CreateSut(); @@ -405,7 +453,7 @@ public async Task CreateDiscoveryDocumentAsync_WhenCalled_ShouldContainCodeChall [Fact] public async Task CreateDiscoveryDocumentAsync_WhenAuthorizeEndpointEnabled_ShouldContainRequestParameterSupported() { - Options.Endpoints.EnableAuthorizeEndpoint = true; + _options.Endpoints.EnableAuthorizeEndpoint = true; var sut = CreateSut(); @@ -418,7 +466,7 @@ public async Task CreateDiscoveryDocumentAsync_WhenAuthorizeEndpointEnabled_Shou [Fact] public async Task CreateDiscoveryDocumentAsync_WhenAuthorizeEndpointDisabled_ShouldNotContainRequestParameterSupported() { - Options.Endpoints.EnableAuthorizeEndpoint = false; + _options.Endpoints.EnableAuthorizeEndpoint = false; var sut = CreateSut(); @@ -430,8 +478,8 @@ public async Task CreateDiscoveryDocumentAsync_WhenAuthorizeEndpointDisabled_Sho [Fact] public async Task CreateDiscoveryDocumentAsync_WhenJwtRequestUriEnabled_ShouldContainRequestUriParameterSupported() { - Options.Endpoints.EnableAuthorizeEndpoint = true; - Options.Endpoints.EnableJwtRequestUri = true; + _options.Endpoints.EnableAuthorizeEndpoint = true; + _options.Endpoints.EnableJwtRequestUri = true; var sut = CreateSut(); @@ -444,8 +492,8 @@ public async Task CreateDiscoveryDocumentAsync_WhenJwtRequestUriEnabled_ShouldCo [Fact] public async Task CreateDiscoveryDocumentAsync_WhenJwtRequestUriDisabled_ShouldNotContainRequestUriParameterSupported() { - Options.Endpoints.EnableAuthorizeEndpoint = true; - Options.Endpoints.EnableJwtRequestUri = false; + _options.Endpoints.EnableAuthorizeEndpoint = true; + _options.Endpoints.EnableJwtRequestUri = false; var sut = CreateSut(); @@ -457,7 +505,7 @@ public async Task CreateDiscoveryDocumentAsync_WhenJwtRequestUriDisabled_ShouldN [Fact] public async Task CreateDiscoveryDocumentAsync_WhenMtlsEnabled_ShouldContainTlsClientCertificateBoundAccessTokens() { - Options.MutualTls.Enabled = true; + _options.MutualTls.Enabled = true; var sut = CreateSut(); @@ -470,7 +518,7 @@ public async Task CreateDiscoveryDocumentAsync_WhenMtlsEnabled_ShouldContainTlsC [Fact] public async Task CreateDiscoveryDocumentAsync_WhenMtlsDisabled_ShouldNotContainTlsClientCertificateBoundAccessTokens() { - Options.MutualTls.Enabled = false; + _options.MutualTls.Enabled = false; var sut = CreateSut(); @@ -482,8 +530,8 @@ public async Task CreateDiscoveryDocumentAsync_WhenMtlsDisabled_ShouldNotContain [Fact] public async Task CreateDiscoveryDocumentAsync_WhenMtlsEnabledWithTokenEndpoint_ShouldContainMtlsEndpointAliases() { - Options.MutualTls.Enabled = true; - Options.Endpoints.EnableTokenEndpoint = true; + _options.MutualTls.Enabled = true; + _options.Endpoints.EnableTokenEndpoint = true; var sut = CreateSut(); @@ -495,9 +543,9 @@ public async Task CreateDiscoveryDocumentAsync_WhenMtlsEnabledWithTokenEndpoint_ [Fact] public async Task CreateDiscoveryDocumentAsync_WhenMtlsEnabledWithDomainName_ShouldUseDomainBasedEndpoints() { - Options.MutualTls.Enabled = true; - Options.MutualTls.DomainName = "mtls.example.com"; - Options.Endpoints.EnableTokenEndpoint = true; + _options.MutualTls.Enabled = true; + _options.MutualTls.DomainName = "mtls.example.com"; + _options.Endpoints.EnableTokenEndpoint = true; var sut = CreateSut(); @@ -511,9 +559,9 @@ public async Task CreateDiscoveryDocumentAsync_WhenMtlsEnabledWithDomainName_Sho [Fact] public async Task CreateDiscoveryDocumentAsync_WhenMtlsEnabledWithSubDomainName_ShouldUseSubDomainBasedEndpoints() { - Options.MutualTls.Enabled = true; - Options.MutualTls.DomainName = "mtls"; - Options.Endpoints.EnableTokenEndpoint = true; + _options.MutualTls.Enabled = true; + _options.MutualTls.DomainName = "mtls"; + _options.Endpoints.EnableTokenEndpoint = true; var sut = CreateSut(); @@ -527,8 +575,8 @@ public async Task CreateDiscoveryDocumentAsync_WhenMtlsEnabledWithSubDomainName_ [Fact] public async Task CreateDiscoveryDocumentAsync_WhenShowKeySetEnabledAndKeysExist_ShouldContainJwksUri() { - Options.Discovery.ShowKeySet = true; - Mock.Get(Keys) + _options.Discovery.ShowKeySet = true; + Mock.Get(_keys) .Setup(x => x.GetValidationKeysAsync()) .ReturnsAsync([new SecurityKeyInfo { Key = new RsaSecurityKey(System.Security.Cryptography.RSA.Create()) }]); @@ -542,8 +590,8 @@ public async Task CreateDiscoveryDocumentAsync_WhenShowKeySetEnabledAndKeysExist [Fact] public async Task CreateDiscoveryDocumentAsync_WhenShowKeySetEnabledAndNoKeys_ShouldNotContainJwksUri() { - Options.Discovery.ShowKeySet = true; - Mock.Get(Keys) + _options.Discovery.ShowKeySet = true; + Mock.Get(_keys) .Setup(x => x.GetValidationKeysAsync()) .ReturnsAsync([]); @@ -557,7 +605,7 @@ public async Task CreateDiscoveryDocumentAsync_WhenShowKeySetEnabledAndNoKeys_Sh [Fact] public async Task CreateDiscoveryDocumentAsync_WhenShowKeySetDisabled_ShouldNotContainJwksUri() { - Options.Discovery.ShowKeySet = false; + _options.Discovery.ShowKeySet = false; var sut = CreateSut(); @@ -569,7 +617,7 @@ public async Task CreateDiscoveryDocumentAsync_WhenShowKeySetDisabled_ShouldNotC [Fact] public async Task CreateDiscoveryDocumentAsync_WhenCustomEntriesConfigured_ShouldContainCustomEntries() { - Options.Discovery.CustomEntries.Add("custom_key", "custom_value"); + _options.Discovery.CustomEntries.Add("custom_key", "custom_value"); var sut = CreateSut(); @@ -582,8 +630,8 @@ public async Task CreateDiscoveryDocumentAsync_WhenCustomEntriesConfigured_Shoul [Fact] public async Task CreateDiscoveryDocumentAsync_WhenCustomEntryWithRelativePath_ShouldExpandPath() { - Options.Discovery.CustomEntries.Add("custom_endpoint", "~/custom"); - Options.Discovery.ExpandRelativePathsInCustomEntries = true; + _options.Discovery.CustomEntries.Add("custom_endpoint", "~/custom"); + _options.Discovery.ExpandRelativePathsInCustomEntries = true; var sut = CreateSut(); @@ -596,7 +644,7 @@ public async Task CreateDiscoveryDocumentAsync_WhenCustomEntryWithRelativePath_S [Fact] public async Task CreateDiscoveryDocumentAsync_WhenCustomEntryConflictsWithExistingKey_ShouldNotOverwrite() { - Options.Discovery.CustomEntries.Add(OidcConstants.Discovery.Issuer, "bad_issuer"); + _options.Discovery.CustomEntries.Add(OidcConstants.Discovery.Issuer, "bad_issuer"); var sut = CreateSut(); @@ -609,11 +657,11 @@ public async Task CreateDiscoveryDocumentAsync_WhenCustomEntryConflictsWithExist [Fact] public async Task CreateDiscoveryDocumentAsync_WhenShowIdentityScopesEnabled_ShouldContainScopesSupported() { - Options.Discovery.ShowIdentityScopes = true; + _options.Discovery.ShowIdentityScopes = true; var sut = CreateSut(); - Mock.Get(ResourceStore) + Mock.Get(_resourceStore) .Setup(x => x.GetAllResourcesAsync()) .ReturnsAsync(new Resources( [ @@ -632,11 +680,11 @@ public async Task CreateDiscoveryDocumentAsync_WhenShowIdentityScopesEnabled_Sho [Fact] public async Task CreateDiscoveryDocumentAsync_WhenShowApiScopesEnabled_ShouldContainOfflineAccess() { - Options.Discovery.ShowApiScopes = true; + _options.Discovery.ShowApiScopes = true; var sut = CreateSut(); - Mock.Get(ResourceStore) + Mock.Get(_resourceStore) .Setup(x => x.GetAllResourcesAsync()) .ReturnsAsync(new Resources( [], @@ -655,11 +703,11 @@ public async Task CreateDiscoveryDocumentAsync_WhenShowApiScopesEnabled_ShouldCo [Fact] public async Task CreateDiscoveryDocumentAsync_WhenShowClaimsEnabled_ShouldContainClaimsSupported() { - Options.Discovery.ShowClaims = true; + _options.Discovery.ShowClaims = true; var sut = CreateSut(); - Mock.Get(ResourceStore) + Mock.Get(_resourceStore) .Setup(x => x.GetAllResourcesAsync()) .ReturnsAsync(new Resources( [ @@ -678,8 +726,8 @@ public async Task CreateDiscoveryDocumentAsync_WhenShowClaimsEnabled_ShouldConta [Fact] public async Task CreateDiscoveryDocumentAsync_WhenShowTokenEndpointAuthMethodsEnabled_ShouldContainAuthMethods() { - Options.Discovery.ShowTokenEndpointAuthenticationMethods = true; - Mock.Get(SecretParsers) + _options.Discovery.ShowTokenEndpointAuthenticationMethods = true; + Mock.Get(_secretParsers) .Setup(x => x.GetAvailableAuthenticationMethods()) .Returns(["client_secret_basic"]); @@ -693,9 +741,9 @@ public async Task CreateDiscoveryDocumentAsync_WhenShowTokenEndpointAuthMethodsE [Fact] public async Task CreateDiscoveryDocumentAsync_WhenMtlsEnabledAndShowAuthMethods_ShouldIncludeTlsAuthMethods() { - Options.Discovery.ShowTokenEndpointAuthenticationMethods = true; - Options.MutualTls.Enabled = true; - Mock.Get(SecretParsers) + _options.Discovery.ShowTokenEndpointAuthenticationMethods = true; + _options.MutualTls.Enabled = true; + Mock.Get(_secretParsers) .Setup(x => x.GetAvailableAuthenticationMethods()) .Returns(["client_secret_basic"]); diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/Default/IntrospectionResponseGeneratorTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/Default/IntrospectionResponseGeneratorTests.cs new file mode 100644 index 000000000..0a574cdaa --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/Default/IntrospectionResponseGeneratorTests.cs @@ -0,0 +1,249 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using AwesomeAssertions; +using Moq; +using Open.IdentityServer.UnitTests.Common; +using Open.IdentityServer.Events; +using Open.IdentityServer.Models; +using Open.IdentityServer.ResponseHandling; +using Open.IdentityServer.Services; +using Open.IdentityServer.Validation; +using Xunit; + +namespace Open.IdentityServer.UnitTests.ResponseHandling.Default; + +public class IntrospectionResponseGeneratorTests +{ + private const string Category = "ResponseHandling"; + + private readonly TestEventService _events = new(); + private readonly Mock _telemetry = new(); + private readonly Mock _trace = new(); + + private IntrospectionResponseGenerator CreateSubject() + { + return new IntrospectionResponseGenerator( + _events, + _telemetry.Object, + TestLogger.Create()); + } + + [Fact] + [Trait("Category", Category)] + public async Task process_should_return_inactive_response_and_raise_success_event_for_inactive_token() + { + var subject = CreateSubject(); + var api = new ApiResource("api1") + { + Scopes = { "scope1" } + }; + + var validationResult = new IntrospectionRequestValidationResult + { + IsActive = false, + Api = api, + Claims = new List() + }; + + var result = await subject.ProcessAsync(validationResult); + + result.Should().NotBeNull(); + result.Should().ContainKey("active"); + result["active"].Should().Be(false); + + _events.AssertEventWasRaised(); + } + + [Fact] + [Trait("Category", Category)] + public async Task process_should_return_emit_telemetry_signal_for_inactive_token() + { + _telemetry.Setup(t => t.CountTokenIntrospection("api1", false)) + .Verifiable(Times.Once); + + var subject = CreateSubject(); + var api = new ApiResource("api1") + { + Scopes = { "scope1" } + }; + + var validationResult = new IntrospectionRequestValidationResult + { + IsActive = false, + Api = api, + Claims = new List() + }; + + await subject.ProcessAsync(validationResult); + + _telemetry.Verify(); + } + + [Fact] + [Trait("Category", Category)] + public async Task process_should_return_inactive_response_and_raise_failure_event_when_expected_scopes_are_missing() + { + var subject = CreateSubject(); + var api = new ApiResource("api1") + { + Scopes = { "scope1" } + }; + + var validationResult = new IntrospectionRequestValidationResult + { + IsActive = true, + Api = api, + Token = "token", + Claims = new List + { + new(JwtClaimTypes.Scope, "different-scope"), + new("sub", "123") + } + }; + + var result = await subject.ProcessAsync(validationResult); + + result.Should().NotBeNull(); + result.Should().ContainKey("active"); + result["active"].Should().Be(false); + + _events.AssertEventWasRaised(); + } + + [Fact] + [Trait("Category", Category)] + public async Task process_should_emit_telemetry_signal_when_expected_scopes_are_missing() + { + _telemetry.Setup(t => t.CountTokenIntrospection("api1", null, "Expected scopes are missing")) + .Verifiable(Times.Once); + + var subject = CreateSubject(); + var api = new ApiResource("api1") + { + Scopes = { "scope1" } + }; + + var validationResult = new IntrospectionRequestValidationResult + { + IsActive = true, + Api = api, + Token = "token", + Claims = new List + { + new(JwtClaimTypes.Scope, "different-scope"), + new("sub", "123") + } + }; + + await subject.ProcessAsync(validationResult); + + _telemetry.Verify(); + } + + [Fact] + [Trait("Category", Category)] + public async Task process_should_return_active_response_and_raise_success_event_when_expected_scopes_are_present() + { + var subject = CreateSubject(); + var api = new ApiResource("api1") + { + Scopes = { "scope1", "scope2" } + }; + + var validationResult = new IntrospectionRequestValidationResult + { + IsActive = true, + Api = api, + Claims = new List + { + new("sub", "123"), + new("client_id", "client"), + new(JwtClaimTypes.Scope, "scope1"), + new(JwtClaimTypes.Scope, "other-scope") + } + }; + + var result = await subject.ProcessAsync(validationResult); + + result.Should().NotBeNull(); + result.Should().ContainKey("active"); + result["active"].Should().Be(true); + result.Should().ContainKey("sub"); + result["sub"].Should().Be("123"); + result.Should().ContainKey("client_id"); + result["client_id"].Should().Be("client"); + result.Should().ContainKey("scope"); + result["scope"].Should().Be("scope1"); + + _events.AssertEventWasRaised(); + } + + [Fact] + [Trait("Category", Category)] + public async Task process_should_emit_telemetry_signal_when_expected_scopes_are_present() + { + _telemetry.Setup(t => t.CountTokenIntrospection( "api1", true, null)) + .Verifiable(Times.Once); + + var subject = CreateSubject(); + var api = new ApiResource("api1") + { + Scopes = { "scope1", "scope2" } + }; + + var validationResult = new IntrospectionRequestValidationResult + { + IsActive = true, + Api = api, + Claims = new List + { + new("sub", "123"), + new("client_id", "client"), + new(JwtClaimTypes.Scope, "scope1"), + new(JwtClaimTypes.Scope, "other-scope") + } + }; + + await subject.ProcessAsync(validationResult); + + _telemetry.Verify(); + } + + [Fact] + public async Task process_should_initiate_telemetry_trace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var subject = CreateSubject(); + var api = new ApiResource("api1") + { + Scopes = { "scope1", "scope2" } + }; + + var validationResult = new IntrospectionRequestValidationResult + { + IsActive = true, + Api = api, + Claims = new List + { + new("sub", "123"), + new("client_id", "client"), + new(JwtClaimTypes.Scope, "scope1"), + new(JwtClaimTypes.Scope, "other-scope") + } + }; + + await subject.ProcessAsync(validationResult); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Basic, + subject, + "ProcessAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/Default/TokenRevocationResponseGeneratorTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/Default/TokenRevocationResponseGeneratorTests.cs new file mode 100644 index 000000000..7a51609ce --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/Default/TokenRevocationResponseGeneratorTests.cs @@ -0,0 +1,64 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Threading.Tasks; +using Moq; +using Open.IdentityServer.Models; +using Open.IdentityServer.ResponseHandling; +using Open.IdentityServer.Services; +using Open.IdentityServer.Stores; +using Open.IdentityServer.UnitTests.Common; +using Open.IdentityServer.Validation; +using Xunit; + +namespace Open.IdentityServer.UnitTests.ResponseHandling.Default; + +public class TokenRevocationResponseGeneratorTests +{ + private Mock _referenceTokenStore; + private Mock _refreshTokenStore; + private Mock _telemetry; + private Mock _trace; + + public TokenRevocationResponseGeneratorTests() + { + _referenceTokenStore = new Mock(); + _refreshTokenStore = new Mock(); + _telemetry = new Mock(); + _trace = new Mock(); + } + + private TokenRevocationResponseGenerator CreateSubject() + { + return new TokenRevocationResponseGenerator( + _referenceTokenStore.Object, + _refreshTokenStore.Object, + _telemetry.Object, + TestLogger.Create()); + } + + [Fact] + public async Task ProcessAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var validatedRequest = new TokenRevocationRequestValidationResult() + { + Client = new Client + { + ClientId = "client1" + } + }; + + var subject = CreateSubject(); + + await subject.ProcessAsync(validatedRequest); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Basic, + subject, + "ProcessAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/UserInfoResponseGeneratorTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/Default/UserInfoResponseGeneratorTests.cs similarity index 83% rename from src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/UserInfoResponseGeneratorTests.cs rename to src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/Default/UserInfoResponseGeneratorTests.cs index 1e61b383c..6f018058a 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/UserInfoResponseGeneratorTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/Default/UserInfoResponseGeneratorTests.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -8,20 +9,24 @@ using System.Text.Json; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; +using Moq; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer; using Open.IdentityServer.Models; using Open.IdentityServer.ResponseHandling; +using Open.IdentityServer.Services; using Open.IdentityServer.Stores; using Open.IdentityServer.Validation; using Xunit; -namespace IdentityServer.UnitTests.ResponseHandling; +namespace Open.IdentityServer.UnitTests.ResponseHandling; public class UserInfoResponseGeneratorTests { private readonly UserInfoResponseGenerator _subject; private readonly MockProfileService _mockProfileService = new(); + private readonly Mock _telemetry = new(); + private readonly Mock _trace = new(); private readonly ClaimsPrincipal _user; private readonly Client _client; @@ -49,7 +54,7 @@ public UserInfoResponseGeneratorTests() }.CreatePrincipal(); _resourceStore = new InMemoryResourcesStore(_identityResources, _apiResources, _apiScopes); - _subject = new UserInfoResponseGenerator(_mockProfileService, _resourceStore, TestLogger.Create()); + _subject = new UserInfoResponseGenerator(_mockProfileService, _resourceStore, _telemetry.Object, TestLogger.Create()); } [Fact] @@ -220,4 +225,34 @@ public async Task ProcessAsync_should_throw_if_incorrect_sub_issued_by_profile_s .And.Message.Should().Contain("subject"); } + [Fact] + public async Task ProcessAsync_whenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + _identityResources.Add(new IdentityResource("id1", ["foo"])); + _identityResources.Add(new IdentityResource("id2", ["bar"])); + + var result = new UserInfoRequestValidationResult + { + Subject = _user, + TokenValidationResult = new TokenValidationResult + { + Claims = new List + { + { new("scope", "id1") }, + { new("scope", "id2") }, + { new("scope", "id3") } + }, + Client = _client + } + }; + + await _subject.ProcessAsync(result); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Basic, _subject, "ProcessAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } } \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/DeviceAuthorizationResponseGeneratorTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/DeviceAuthorizationResponseGeneratorTests.cs deleted file mode 100644 index 33f001596..000000000 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/DeviceAuthorizationResponseGeneratorTests.cs +++ /dev/null @@ -1,205 +0,0 @@ -// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using AwesomeAssertions; -using IdentityServer.UnitTests.Common; -using Open.IdentityServer.Configuration; -using Open.IdentityServer.Models; -using Open.IdentityServer.ResponseHandling; -using Open.IdentityServer.Services; -using Open.IdentityServer.Services.Default; -using Open.IdentityServer.Stores; -using Open.IdentityServer.Validation; -using Microsoft.Extensions.Logging.Abstractions; -using Xunit; - -namespace IdentityServer.UnitTests.ResponseHandling; - -public class DeviceAuthorizationResponseGeneratorTests -{ - private readonly List identityResources = - [new IdentityResources.OpenId(), new IdentityResources.Profile()]; - private readonly List apiResources = [new("resource") { Scopes = { "api1" } }]; - private readonly List scopes = [new("api1")]; - - private readonly FakeUserCodeGenerator fakeUserCodeGenerator = new(); - private readonly IDeviceFlowCodeService deviceFlowCodeService = new DefaultDeviceFlowCodeService(new InMemoryDeviceFlowStore(), new StubHandleGenerationService()); - private readonly IdentityServerOptions options = new(); - private readonly StubClock clock = new(); - - private readonly DeviceAuthorizationResponseGenerator generator; - private readonly DeviceAuthorizationRequestValidationResult testResult; - private const string TestBaseUrl = "http://localhost:5000/"; - - public DeviceAuthorizationResponseGeneratorTests() - { - testResult = new DeviceAuthorizationRequestValidationResult(new ValidatedDeviceAuthorizationRequest - { - Client = new Client {ClientId = Guid.NewGuid().ToString()}, - IsOpenIdRequest = true, - ValidatedResources = new ResourceValidationResult() - }); - - generator = new DeviceAuthorizationResponseGenerator( - options, - new DefaultUserCodeService([new NumericUserCodeGenerator(), fakeUserCodeGenerator]), - deviceFlowCodeService, - clock, - new NullLogger()); - } - - [Fact] - public async Task ProcessAsync_when_valiationresult_null_exect_exception() - { - Func act = () => generator.ProcessAsync(null, TestBaseUrl); - await act.Should().ThrowAsync(); - } - - [Fact] - public async Task ProcessAsync_when_valiationresult_client_null_exect_exception() - { - var validationResult = new DeviceAuthorizationRequestValidationResult(new ValidatedDeviceAuthorizationRequest()); - Func act = () => generator.ProcessAsync(validationResult, TestBaseUrl); - await act.Should().ThrowAsync(); - } - - [Fact] - public async Task ProcessAsync_when_baseurl_null_exect_exception() - { - Func act = () => generator.ProcessAsync(testResult, null); - await act.Should().ThrowAsync(); - } - - [Fact] - public async Task ProcessAsync_when_user_code_collision_expect_retry() - { - var creationTime = DateTime.UtcNow; - clock.UtcNowFunc = () => creationTime; - - testResult.ValidatedRequest.Client.UserCodeType = FakeUserCodeGenerator.UserCodeTypeValue; - await deviceFlowCodeService.StoreDeviceAuthorizationAsync(FakeUserCodeGenerator.TestCollisionUserCode, new DeviceCode()); - - var response = await generator.ProcessAsync(testResult, TestBaseUrl); - - response.UserCode.Should().Be(FakeUserCodeGenerator.TestUniqueUserCode); - } - - [Fact] - public async Task ProcessAsync_when_user_code_collision_retry_limit_reached_expect_error() - { - var creationTime = DateTime.UtcNow; - clock.UtcNowFunc = () => creationTime; - - fakeUserCodeGenerator.RetryLimit = 1; - testResult.ValidatedRequest.Client.UserCodeType = FakeUserCodeGenerator.UserCodeTypeValue; - await deviceFlowCodeService.StoreDeviceAuthorizationAsync(FakeUserCodeGenerator.TestCollisionUserCode, new DeviceCode()); - - await Assert.ThrowsAsync(() => generator.ProcessAsync(testResult, TestBaseUrl)); - } - - [Fact] - public async Task ProcessAsync_when_generated_expect_user_code_stored() - { - var creationTime = DateTime.UtcNow; - clock.UtcNowFunc = () => creationTime; - - testResult.ValidatedRequest.RequestedScopes = new List { "openid", "api1" }; - testResult.ValidatedRequest.ValidatedResources = new ResourceValidationResult(new Resources( - identityResources.Where(x=>x.Name == "openid"), - apiResources.Where(x=>x.Name == "resource"), - scopes.Where(x=>x.Name == "api1"))); - - var response = await generator.ProcessAsync(testResult, TestBaseUrl); - - response.UserCode.Should().NotBeNullOrWhiteSpace(); - - var userCode = await deviceFlowCodeService.FindByUserCodeAsync(response.UserCode); - userCode.Should().NotBeNull(); - userCode.ClientId.Should().Be(testResult.ValidatedRequest.Client.ClientId); - userCode.Lifetime.Should().Be(testResult.ValidatedRequest.Client.DeviceCodeLifetime); - userCode.CreationTime.Should().Be(creationTime); - userCode.Subject.Should().BeNull(); - userCode.AuthorizedScopes.Should().BeNull(); - - userCode.RequestedScopes.Should().Contain(testResult.ValidatedRequest.RequestedScopes); - } - - [Fact] - public async Task ProcessAsync_when_generated_expect_device_code_stored() - { - var creationTime = DateTime.UtcNow; - clock.UtcNowFunc = () => creationTime; - - var response = await generator.ProcessAsync(testResult, TestBaseUrl); - - response.DeviceCode.Should().NotBeNullOrWhiteSpace(); - response.Interval.Should().Be(options.DeviceFlow.Interval); - - var deviceCode = await deviceFlowCodeService.FindByDeviceCodeAsync(response.DeviceCode); - deviceCode.Should().NotBeNull(); - deviceCode.ClientId.Should().Be(testResult.ValidatedRequest.Client.ClientId); - deviceCode.IsOpenId.Should().Be(testResult.ValidatedRequest.IsOpenIdRequest); - deviceCode.Lifetime.Should().Be(testResult.ValidatedRequest.Client.DeviceCodeLifetime); - deviceCode.CreationTime.Should().Be(creationTime); - deviceCode.Subject.Should().BeNull(); - deviceCode.AuthorizedScopes.Should().BeNull(); - - response.DeviceCodeLifetime.Should().Be(deviceCode.Lifetime); - } - - [Fact] - public async Task ProcessAsync_when_DeviceVerificationUrl_is_relative_uri_expect_correct_VerificationUris() - { - const string baseUrl = "http://localhost:5000/"; - options.UserInteraction.DeviceVerificationUrl = "/device"; - options.UserInteraction.DeviceVerificationUserCodeParameter = "userCode"; - - var response = await generator.ProcessAsync(testResult, baseUrl); - - response.VerificationUri.Should().Be("http://localhost:5000/device"); - response.VerificationUriComplete.Should().StartWith("http://localhost:5000/device?userCode="); - } - - [Fact] - public async Task ProcessAsync_when_DeviceVerificationUrl_is_absolute_uri_expect_correct_VerificationUris() - { - const string baseUrl = "http://localhost:5000/"; - options.UserInteraction.DeviceVerificationUrl = "http://short/device"; - options.UserInteraction.DeviceVerificationUserCodeParameter = "userCode"; - - var response = await generator.ProcessAsync(testResult, baseUrl); - - response.VerificationUri.Should().Be("http://short/device"); - response.VerificationUriComplete.Should().StartWith("http://short/device?userCode="); - } -} - -internal class FakeUserCodeGenerator : IUserCodeGenerator -{ - public const string UserCodeTypeValue = "Collider"; - public const string TestUniqueUserCode = "123"; - public const string TestCollisionUserCode = "321"; - private int tryCount; - - - public string UserCodeType => UserCodeTypeValue; - - public int RetryLimit { get; set; } = 2; - - public Task GenerateAsync() - { - if (tryCount == 0) - { - tryCount++; - return Task.FromResult(TestCollisionUserCode); - } - - tryCount++; - return Task.FromResult(TestUniqueUserCode); - } -} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/TokenResponseGenerator/TokenResponseGeneratorTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/TokenResponseGenerator/TokenResponseGeneratorTests.cs index 39c1ec95c..8ffd5a510 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/TokenResponseGenerator/TokenResponseGeneratorTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/TokenResponseGenerator/TokenResponseGeneratorTests.cs @@ -6,7 +6,8 @@ using System; using System.Linq; using System.Security.Claims; -using IdentityServer.UnitTests.Validation.Setup; +using System.Threading.Tasks; +using Open.IdentityServer.UnitTests.Validation.Setup; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Time.Testing; @@ -16,8 +17,9 @@ using Open.IdentityServer.Services; using Open.IdentityServer.Stores; using Open.IdentityServer.Validation; +using Xunit; -namespace IdentityServer.UnitTests.ResponseHandling.TokenResponseGenerator; +namespace Open.IdentityServer.UnitTests.ResponseHandling.TokenResponseGenerator; public abstract class TokenResponseGeneratorTests { @@ -27,6 +29,7 @@ public abstract class TokenResponseGeneratorTests protected readonly IScopeParser scopeParser = new DefaultScopeParser(NullLogger.Instance); protected readonly IResourceStore resources = new InMemoryResourcesStore(TestScopes.GetIdentity(), TestScopes.GetApis(), TestScopes.GetScopes()); protected readonly IClientStore clients = new InMemoryClientStore(TestClients.Get()); + protected readonly ITelemetryService telemetry = Mock.Of(); protected readonly ILogger logger = NullLogger.Instance; protected readonly DateTime FakeNow = new DateTime(2026, 02, 01, 12, 23, 00, DateTimeKind.Utc); @@ -37,7 +40,7 @@ protected TokenResponseGeneratorTests() } protected Open.IdentityServer.ResponseHandling.TokenResponseGenerator CreateSut() => - new(clock, tokenService, refreshTokenService, scopeParser, resources, clients, logger); + new(clock, tokenService, refreshTokenService, scopeParser, resources, clients, telemetry, logger); protected class CapturedTokenCreationRequest { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/TokenResponseGenerator/TokenResponseGeneratorTests_ClientCredentials.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/TokenResponseGenerator/TokenResponseGeneratorTests_ClientCredentials.cs index 1c1c86833..4d8ef875c 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/TokenResponseGenerator/TokenResponseGeneratorTests_ClientCredentials.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/TokenResponseGenerator/TokenResponseGeneratorTests_ClientCredentials.cs @@ -9,7 +9,7 @@ using Open.IdentityServer.Validation; using Xunit; -namespace IdentityServer.UnitTests.ResponseHandling.TokenResponseGenerator; +namespace Open.IdentityServer.UnitTests.ResponseHandling.TokenResponseGenerator; public class TokenResponseGeneratorTests_ClientCredentials : TokenResponseGeneratorTests { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/TokenResponseGenerator/TokenResponseGeneratorTests_Code.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/TokenResponseGenerator/TokenResponseGeneratorTests_Code.cs index 29346c1f8..ca5811fa2 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/TokenResponseGenerator/TokenResponseGeneratorTests_Code.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/TokenResponseGenerator/TokenResponseGeneratorTests_Code.cs @@ -9,7 +9,7 @@ using Open.IdentityServer.Validation; using Xunit; -namespace IdentityServer.UnitTests.ResponseHandling.TokenResponseGenerator; +namespace Open.IdentityServer.UnitTests.ResponseHandling.TokenResponseGenerator; public class TokenResponseGeneratorTests_Code : TokenResponseGeneratorTests { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/TokenResponseGenerator/TokenResponseGeneratorTests_DeviceCode.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/TokenResponseGenerator/TokenResponseGeneratorTests_DeviceCode.cs index d4df21ab4..6edbd3356 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/TokenResponseGenerator/TokenResponseGeneratorTests_DeviceCode.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/TokenResponseGenerator/TokenResponseGeneratorTests_DeviceCode.cs @@ -4,15 +4,45 @@ using System; using System.Threading.Tasks; using AwesomeAssertions; +using Moq; using Open.IdentityServer; using Open.IdentityServer.Models; +using Open.IdentityServer.Services; using Open.IdentityServer.Validation; using Xunit; -namespace IdentityServer.UnitTests.ResponseHandling.TokenResponseGenerator; +namespace Open.IdentityServer.UnitTests.ResponseHandling.TokenResponseGenerator; public class TokenResponseGeneratorTests_DeviceCode : TokenResponseGeneratorTests { + [Fact] + public async Task ProcessAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + var trace = Mock.Of(); + Mock.Get(telemetry).Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(trace); + + var subject = CreateSut(); + + await subject.ProcessAsync( + new TokenRequestValidationResult( + new ValidatedTokenRequest() + { + GrantType = "grant", + })); + + Mock.Get(telemetry) + .Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Basic, + subject, + "ProcessAsync")); + Mock.Get(trace) + .Verify(t => t.Dispose(), Times.Once); + Mock.Get(trace) + .Verify(t => t.AddTag(TelemetryConstants.TagConstants.GrantType, "grant"), + Times.Once); + } + [Fact] public async Task ProcessAsync_DeviceCode_ReturnsAccessToken() { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/TokenResponseGenerator/TokenResponseGeneratorTests_ExtentionGrant.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/TokenResponseGenerator/TokenResponseGeneratorTests_ExtentionGrant.cs index 6789f8c88..d8f41888b 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/TokenResponseGenerator/TokenResponseGeneratorTests_ExtentionGrant.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/TokenResponseGenerator/TokenResponseGeneratorTests_ExtentionGrant.cs @@ -7,7 +7,7 @@ using Open.IdentityServer.Validation; using Xunit; -namespace IdentityServer.UnitTests.ResponseHandling.TokenResponseGenerator; +namespace Open.IdentityServer.UnitTests.ResponseHandling.TokenResponseGenerator; public class TokenResponseGeneratorTests_ExtentionGrant: TokenResponseGeneratorTests { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/TokenResponseGenerator/TokenResponseGeneratorTests_PasswordGrant.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/TokenResponseGenerator/TokenResponseGeneratorTests_PasswordGrant.cs index 6dd8ade62..21744dfa0 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/TokenResponseGenerator/TokenResponseGeneratorTests_PasswordGrant.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/TokenResponseGenerator/TokenResponseGeneratorTests_PasswordGrant.cs @@ -8,7 +8,7 @@ using Open.IdentityServer.Validation; using Xunit; -namespace IdentityServer.UnitTests.ResponseHandling.TokenResponseGenerator; +namespace Open.IdentityServer.UnitTests.ResponseHandling.TokenResponseGenerator; public class TokenResponseGeneratorTests_PasswordGrant: TokenResponseGeneratorTests { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/TokenResponseGenerator/TokenResponseGeneratorTests_RefreshToken.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/TokenResponseGenerator/TokenResponseGeneratorTests_RefreshToken.cs index 6e25c4ce3..b2717bc98 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/TokenResponseGenerator/TokenResponseGeneratorTests_RefreshToken.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/ResponseHandling/TokenResponseGenerator/TokenResponseGeneratorTests_RefreshToken.cs @@ -13,7 +13,7 @@ using Open.IdentityServer.Validation; using Xunit; -namespace IdentityServer.UnitTests.ResponseHandling.TokenResponseGenerator; +namespace Open.IdentityServer.UnitTests.ResponseHandling.TokenResponseGenerator; public class TokenResponseGeneratorTests_RefreshToken : TokenResponseGeneratorTests { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultBackChannelLogoutHttpClientTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultBackChannelLogoutHttpClientTests.cs new file mode 100644 index 000000000..ba9047bf9 --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultBackChannelLogoutHttpClientTests.cs @@ -0,0 +1,59 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Moq; +using Moq.Protected; +using Open.IdentityServer.Services; +using Open.IdentityServer.UnitTests.Common; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Services.Default; + +public class DefaultBackChannelLogoutHttpClientTests +{ + private readonly HttpClient _client; + private readonly Mock _handler; + private readonly Mock _telemetry; + private readonly Mock _trace; + private readonly Mock _loggerFactory; + + public DefaultBackChannelLogoutHttpClientTests() + { + _handler = new Mock(); + _client = new HttpClient(_handler.Object); + + _loggerFactory = new Mock(); + _loggerFactory.Setup(lf => lf.CreateLogger(It.IsAny())).Returns(TestLogger.Create()); + + _telemetry = new Mock(); + _trace = new Mock(); + } + + private DefaultBackChannelLogoutHttpClient CreateSubject() + { + return new DefaultBackChannelLogoutHttpClient( + _client, + _telemetry.Object, + _loggerFactory.Object); + } + + [Fact] + public async Task PostAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var subject = CreateSubject(); + + await subject.PostAsync("https://example.com/logout", new Dictionary { { "logout_token", "test-token" } }); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, subject, "PostAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultBackChannelLogoutServiceTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultBackChannelLogoutServiceTests.cs new file mode 100644 index 000000000..55fb2edf3 --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultBackChannelLogoutServiceTests.cs @@ -0,0 +1,81 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Moq; +using Open.IdentityServer.Models; +using Open.IdentityServer.Services; +using Open.IdentityServer.UnitTests.Common; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Services.Default; + +public class DefaultBackChannelLogoutServiceTests +{ + private readonly TimeProvider _clock = new StubClock(); + private readonly Mock _tokenCreation = new(); + private readonly IdentityServerTools _tools; + private readonly Mock _logoutNotification = new(); + private readonly Mock _backChannelLogoutHttpClient = new(); + private readonly Mock _telemetry = new(); + private readonly Mock _trace = new(); + + public DefaultBackChannelLogoutServiceTests() + { + _tools = new MockIdentityServerTools(Mock.Of(), _tokenCreation.Object, _clock); + } + + private DefaultBackChannelLogoutService CreateSubject() + { + return new DefaultBackChannelLogoutService( + _clock, + _tools, + _logoutNotification.Object, + _backChannelLogoutHttpClient.Object, + _telemetry.Object, + TestLogger.Create() + ); + } + + [Fact] + public async Task SendLogoutNotificationAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + _logoutNotification.Setup(ln => ln.GetBackChannelLogoutNotificationsAsync(It.IsAny())) + .ReturnsAsync([]); + + var subject = CreateSubject(); + + await subject.SendLogoutNotificationsAsync(new LogoutNotificationContext()); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, subject, "SendLogoutNotificationsAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } +} + +internal class MockIdentityServerTools : IdentityServerTools +{ + private readonly ITokenCreationService _tokenCreation; + + public MockIdentityServerTools(IHttpContextAccessor contextAccessor, ITokenCreationService tokenCreation, TimeProvider clock) : base(contextAccessor, tokenCreation, clock) + { + _tokenCreation = tokenCreation; + } + + public override Task IssueJwtAsync(int lifetime, IEnumerable claims) + { + return _tokenCreation.CreateTokenAsync(new Token()); + } + + public override Task IssueJwtAsync(int lifetime, string issuer, IEnumerable claims) + { + return _tokenCreation.CreateTokenAsync(new Token()); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultClaimsServiceTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultClaimsServiceTests.cs index 215078870..cd2a7d252 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultClaimsServiceTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultClaimsServiceTests.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -6,7 +7,8 @@ using System.Security.Claims; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; +using Moq; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer; using Open.IdentityServer.Configuration; using Open.IdentityServer.Models; @@ -14,12 +16,14 @@ using Open.IdentityServer.Validation; using Xunit; -namespace IdentityServer.UnitTests.Services.Default; +namespace Open.IdentityServer.UnitTests.Services.Default; public class DefaultClaimsServiceTests { private DefaultClaimsService _subject; private MockProfileService _mockMockProfileService = new MockProfileService(); + private Mock _telemetry = new(); + private Mock _trace = new(); private ClaimsPrincipal _user; private Client _client; @@ -51,7 +55,7 @@ public DefaultClaimsServiceTests() } }.CreatePrincipal(); - _subject = new DefaultClaimsService(_mockMockProfileService, TestLogger.Create()); + _subject = new DefaultClaimsService(_mockMockProfileService, _telemetry.Object, TestLogger.Create()); _validatedRequest = new ValidatedRequest(); _validatedRequest.Options = new IdentityServerOptions(); @@ -365,4 +369,30 @@ public async Task GetAccessTokenClaimsAsync_should_request_both_api_and_api_scop _mockMockProfileService.ProfileContext.RequestedClaimTypes.Should().Contain("foo"); _mockMockProfileService.ProfileContext.RequestedClaimTypes.Should().Contain("bar"); } + + [Fact] + public async Task GetIdentityTokenClaimsAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + await _subject.GetIdentityTokenClaimsAsync(_user, ResourceValidationResult, true, _validatedRequest); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, _subject, "GetIdentityTokenClaimsAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task GetAccessTokenClaimsAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + await _subject.GetAccessTokenClaimsAsync(_user, ResourceValidationResult, _validatedRequest); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, _subject, "GetAccessTokenClaimsAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } } \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultConsentServiceTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultConsentServiceTests.cs index 6c03d304d..423c709fe 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultConsentServiceTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultConsentServiceTests.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -7,7 +8,8 @@ using System.Security.Claims; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; +using Moq; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer; using Open.IdentityServer.Extensions; using Open.IdentityServer.Models; @@ -15,7 +17,7 @@ using Open.IdentityServer.Validation; using Xunit; -namespace IdentityServer.UnitTests.Services.Default; +namespace Open.IdentityServer.UnitTests.Services.Default; public class DefaultConsentServiceTests { @@ -26,6 +28,8 @@ public class DefaultConsentServiceTests private Client _client; private TestUserConsentStore _userConsentStore = new TestUserConsentStore(); private StubClock _clock = new StubClock(); + private Mock _telemetry = new(); + private Mock _trace = new(); private DateTime now; @@ -52,7 +56,7 @@ public DefaultConsentServiceTests() } }.CreatePrincipal(); - _subject = new DefaultConsentService(_clock, _userConsentStore, TestLogger.Create()); + _subject = new DefaultConsentService(_clock, _userConsentStore, _telemetry.Object, TestLogger.Create()); } public DateTime UtcNow @@ -206,4 +210,30 @@ public async Task RequiresConsentAsync_expired_consent_should_remove_consent() result.Should().BeNull(); } + + [Fact] + public async Task UpdateConsentAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + await _subject.UpdateConsentAsync(_user, _client, new[] { new ParsedScopeValue("scope1"), new ParsedScopeValue("scope2") }); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, _subject, "UpdateConsentAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task RequireConsentAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + await _subject.RequiresConsentAsync(_user, _client, new[] { new ParsedScopeValue("scope1"), new ParsedScopeValue("scope2") }); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, _subject, "RequiresConsentAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } } \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultCorsPolicyServiceTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultCorsPolicyServiceTests.cs index 0aa851f9c..5f10ae01a 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultCorsPolicyServiceTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultCorsPolicyServiceTests.cs @@ -5,11 +5,11 @@ using System; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer.Services; using Xunit; -namespace IdentityServer.UnitTests.Services.Default; +namespace Open.IdentityServer.UnitTests.Services.Default; public class DefaultCorsPolicyServiceTests { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultDeviceFlowCodeServiceTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultDeviceFlowCodeServiceTests.cs new file mode 100644 index 000000000..5139c35ab --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultDeviceFlowCodeServiceTests.cs @@ -0,0 +1,106 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Threading.Tasks; +using Moq; +using Open.IdentityServer.Services; +using Open.IdentityServer.Services.Default; +using Open.IdentityServer.Stores; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Services.Default; + +public class DefaultDeviceFlowCodeServiceTests +{ + private readonly Mock _store = new(); + private readonly Mock _handle = new(); + private readonly Mock _telemetry = new(); + private readonly Mock _trace = new(); + + public DefaultDeviceFlowCodeServiceTests() + { + _handle.Setup(h => h.GenerateAsync()).ReturnsAsync("handle"); + } + + private DefaultDeviceFlowCodeService CreateSubject() + { + return new DefaultDeviceFlowCodeService( + _store.Object, + _handle.Object, + _telemetry.Object); + } + + [Fact] + public async Task StoreDeviceAuthorizationAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var subject = CreateSubject(); + + await subject.StoreDeviceAuthorizationAsync("userCode", new Models.DeviceCode()); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, subject, "StoreDeviceAuthorizationAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task FindByUserCodeAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var subject = CreateSubject(); + + await subject.FindByUserCodeAsync("userCode"); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, subject, "FindByUserCodeAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task FindByDeviceCodeAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var subject = CreateSubject(); + + await subject.FindByDeviceCodeAsync("deviceCode"); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, subject, "FindByDeviceCodeAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task UpdateByUserCodeAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var subject = CreateSubject(); + + await subject.UpdateByUserCodeAsync("userCode", new Models.DeviceCode()); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, subject, "UpdateByUserCodeAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task RemoveByDeviceCodeAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + var subject = CreateSubject(); + + await subject.RemoveByDeviceCodeAsync("deviceCode"); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, subject, "RemoveByDeviceCodeAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultDeviceFlowInteractionServiceTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultDeviceFlowInteractionServiceTests.cs new file mode 100644 index 000000000..35690dd67 --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultDeviceFlowInteractionServiceTests.cs @@ -0,0 +1,71 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Threading.Tasks; +using Moq; +using Open.IdentityServer.Models; +using Open.IdentityServer.Services; +using Open.IdentityServer.Stores; +using Open.IdentityServer.UnitTests.Common; +using Open.IdentityServer.Validation; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Services.Default; + +public class DefaultDeviceFlowInteractionServiceTests +{ + private readonly Mock _clientStore = new(); + private readonly Mock _userSession = new(); + private readonly Mock _deviceFlowCodeService = new(); + private readonly Mock _resourceStore = new(); + private readonly Mock _scopeParser = new(); + private readonly Mock _telemetry = new(); + private readonly Mock _trace = new(); + + public DefaultDeviceFlowInteractionServiceTests() + { + } + + private DefaultDeviceFlowInteractionService CreateSubject() + { + return new DefaultDeviceFlowInteractionService( + _clientStore.Object, + _userSession.Object, + _deviceFlowCodeService.Object, + _resourceStore.Object, + _scopeParser.Object, + _telemetry.Object, + TestLogger.Create() + ); + } + + [Fact] + public async Task GetAuthorizationContextAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var subject = CreateSubject(); + + await subject.GetAuthorizationContextAsync("test"); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, subject, "GetAuthorizationContextAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task HandleRequestAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var subject = CreateSubject(); + + await subject.HandleRequestAsync("test", new ConsentResponse()); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, subject, "HandleRequestAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultIdentityServerInteractionServiceTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultIdentityServerInteractionServiceTests.cs index 94ea9b2d0..0cb1ed8de 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultIdentityServerInteractionServiceTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultIdentityServerInteractionServiceTests.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -6,7 +7,8 @@ using System.Linq; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; +using Moq; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer; using Open.IdentityServer.Configuration; using Open.IdentityServer.Models; @@ -14,7 +16,7 @@ using Open.IdentityServer.Validation; using Xunit; -namespace IdentityServer.UnitTests.Services.Default; +namespace Open.IdentityServer.UnitTests.Services.Default; public class DefaultIdentityServerInteractionServiceTests { @@ -29,6 +31,8 @@ public class DefaultIdentityServerInteractionServiceTests private readonly MockPersistedGrantService _mockPersistedGrantService = new(); private readonly MockUserSession _mockUserSession = new(); private readonly MockReturnUrlParser _mockReturnUrlParser = new(); + private readonly Mock _telemetry = new(); + private readonly Mock _trace = new(); private readonly ResourceValidationResult _resourceValidationResult; @@ -44,6 +48,7 @@ public DefaultIdentityServerInteractionServiceTests() _mockPersistedGrantService, _mockUserSession, _mockReturnUrlParser, + _telemetry.Object, TestLogger.Create() ); @@ -145,4 +150,172 @@ public async Task GrantConsentAsync_should_use_current_subject_and_create_messag var consentRequest = new ConsentRequest(req, "bob"); _mockConsentStore.Messages.First().Key.Should().Be(consentRequest.Id); } + + [Fact] + public async Task GetAuthorizationContextAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + await _subject.GetAuthorizationContextAsync("return-url"); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + _subject, + nameof(_subject.GetAuthorizationContextAsync))); + _trace.Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public void IsValidReturnUrl_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + _subject.IsValidReturnUrl("return-url"); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + _subject, + nameof(_subject.IsValidReturnUrl))); + _trace.Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task GetErrorContextAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + await _subject.GetErrorContextAsync("error-id"); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + _subject, + nameof(_subject.GetErrorContextAsync))); + _trace.Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task GetLogoutContextAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + await _subject.GetLogoutContextAsync("logout-id"); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + _subject, + nameof(_subject.GetLogoutContextAsync))); + _trace.Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task CreateLogoutContextAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + await _subject.CreateLogoutContextAsync(); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + _subject, + nameof(_subject.CreateLogoutContextAsync))); + _trace.Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task GrantConsentAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var request = CreateValidRequest(); + await _subject.GrantConsentAsync(request, new ConsentResponse(), "subject"); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + _subject, + nameof(_subject.GrantConsentAsync))); + _trace.Verify(t => t.Dispose(), Times.Once); + } + + private AuthorizationRequest CreateValidRequest() + { + return new AuthorizationRequest() + { + Client = new Client { ClientId = "client" }, + ValidatedResources = _resourceValidationResult + }; + } + + [Fact] + public async Task DenyAuthorizationAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + int stackDepth = 0; + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(() => + { + var toReturn = _trace.Object; + if (stackDepth > 0) toReturn = Mock.Of(); + stackDepth++; + return toReturn; + }); + + var request = CreateValidRequest(); + await _subject.DenyAuthorizationAsync(request, AuthorizationError.AccessDenied); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + _subject, + nameof(_subject.DenyAuthorizationAsync))); + _trace.Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task GetAllUserGrantsAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + await _subject.GetAllUserGrantsAsync(); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + _subject, + nameof(_subject.GetAllUserGrantsAsync))); + _trace.Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task RevokeUserConsentAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + await _subject.RevokeUserConsentAsync("client-id"); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + _subject, + nameof(_subject.RevokeUserConsentAsync))); + _trace.Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task RevokeTokensForCurrentSessionAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + await _subject.RevokeTokensForCurrentSessionAsync(); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + _subject, + nameof(_subject.RevokeTokensForCurrentSessionAsync))); + _trace.Verify(t => t.Dispose(), Times.Once); + } } \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultJwtRequestUriHttpClientTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultJwtRequestUriHttpClientTests.cs new file mode 100644 index 000000000..8bf334784 --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultJwtRequestUriHttpClientTests.cs @@ -0,0 +1,73 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Moq; +using Moq.Protected; +using Open.IdentityServer.Configuration; +using Open.IdentityServer.Services; +using Open.IdentityServer.UnitTests.Common; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Services; + +public class DefaultJwtRequestUriHttpClientTests +{ + private readonly HttpClient _client; + private readonly IdentityServerOptions _options; + private readonly Mock _loggerFactory; + private readonly Mock _handler; + private readonly Mock _telemetry; + private readonly Mock _trace; + + public DefaultJwtRequestUriHttpClientTests() + { + _handler = new Mock(); + _client = new HttpClient(_handler.Object); + + _options = new IdentityServerOptions(); + _loggerFactory = new Mock(); + _loggerFactory.Setup(lf => lf.CreateLogger(It.IsAny())).Returns(TestLogger.Create()); + + _telemetry = new Mock(); + _trace = new Mock(); + } + + private DefaultJwtRequestUriHttpClient CreateSubject() + { + return new DefaultJwtRequestUriHttpClient( + _client, + _options, + _telemetry.Object, + _loggerFactory.Object + ); + } + + [Fact] + public async Task GetJwtAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + _handler + .Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent("{\"key\":\"value\"}", System.Text.Encoding.UTF8, "application/jwt") + }); + + var subject = CreateSubject(); + + await subject.GetJwtAsync("https://example.com/jwt", new Models.Client { ClientId = "test-client" }); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + subject, + "GetJwtAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultKeyMaterialServiceTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultKeyMaterialServiceTests.cs index f4bb674e5..a6d179057 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultKeyMaterialServiceTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultKeyMaterialServiceTests.cs @@ -6,6 +6,8 @@ using System.Threading.Tasks; using AwesomeAssertions; using Microsoft.IdentityModel.Tokens; +using Moq; +using Open.IdentityServer; using Open.IdentityServer.Models; using Open.IdentityServer.Services; using Open.IdentityServer.Stores; @@ -54,7 +56,12 @@ public class DefaultKeyMaterialServiceTests private static SecurityKeyInfo fakeSecurityKey8 = new() { Key = new ECDsaSecurityKey(ECDsa.Create(ECCurve.NamedCurves.nistP256)) , SigningAlgorithm = "ES256" }; private static SecurityKeyInfo fakeSecurityKey9 = new() { Key = new ECDsaSecurityKey(ECDsa.Create(ECCurve.NamedCurves.nistP256)) , SigningAlgorithm = "ES384" }; - private DefaultKeyMaterialService CreateSut() => new(validationKeysStores, signingCredentialStores); + + private Mock _telemetry = new(); + private Mock _trace = new(); + + + private DefaultKeyMaterialService CreateSut() => new(validationKeysStores, signingCredentialStores, _telemetry.Object); [Fact] public async Task GetSigningCredentialsAsync_WhenNoSigningCredentialStores_ShouldReturnNull() @@ -153,4 +160,55 @@ public async Task GetValidationKeysAsync_ShouldReturnResultsFromAllRegisteredSto fakeSecurityKey7, fakeSecurityKey8, fakeSecurityKey9, ]); } + + [Fact] + public async Task GetValidationKeysAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var subject = CreateSut(); + + await subject.GetValidationKeysAsync(); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + subject, + "GetValidationKeysAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task GetSigningCredentialsAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var subject = CreateSut(); + + await subject.GetSigningCredentialsAsync(); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + subject, + "GetSigningCredentialsAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task GetAllSigningCredentialsASync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var subject = CreateSut(); + + await subject.GetAllSigningCredentialsAsync(); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + subject, + "GetAllSigningCredentialsAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } } \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultPersistedGrantServiceTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultPersistedGrantServiceTests.cs index 9eb3a6145..2398887c8 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultPersistedGrantServiceTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultPersistedGrantServiceTests.cs @@ -8,7 +8,7 @@ using System.Security.Claims; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer; using Open.IdentityServer.Models; using Open.IdentityServer.Services; @@ -16,9 +16,10 @@ using Open.IdentityServer.Stores.Serialization; using Microsoft.Extensions.Logging; using Moq; +using Open.IdentityServer.UnitTests.Validation.Setup; using Xunit; -namespace IdentityServer.UnitTests.Services.Default; +namespace Open.IdentityServer.UnitTests.Services.Default; public class DefaultPersistedGrantServiceTests { @@ -31,27 +32,33 @@ public class DefaultPersistedGrantServiceTests private IPersistentGrantSerializer _persistentGrantSerializer = new PersistentGrantSerializer(); private ILogger _logger = Mock.Of>(); + private ITelemetryService _telemetry = Mock.Of(); + private ITrace _trace = Mock.Of(); private ClaimsPrincipal _user = new IdentityServerUser("123").CreatePrincipal(); public DefaultPersistedGrantServiceTests() { - _subject = new DefaultPersistedGrantService(_store, _persistentGrantSerializer, _logger); + _subject = new DefaultPersistedGrantService(_store, _persistentGrantSerializer, _telemetry, _logger); _codes = new DefaultAuthorizationCodeStore(_store, _persistentGrantSerializer, new DefaultHandleGenerationService(), + new NopTelemetryService(), TestLogger.Create()); _refreshTokens = new DefaultRefreshTokenStore(_store, _persistentGrantSerializer, new DefaultHandleGenerationService(), + new NopTelemetryService(), TestLogger.Create()); _referenceTokens = new DefaultReferenceTokenStore(_store, _persistentGrantSerializer, new DefaultHandleGenerationService(), + new NopTelemetryService(), TestLogger.Create()); _userConsent = new DefaultUserConsentStore(_store, _persistentGrantSerializer, new DefaultHandleGenerationService(), + new NopTelemetryService(), TestLogger.Create()); } @@ -198,6 +205,22 @@ await _userConsent.StoreUserConsentAsync(new Consent grant2.Scopes.Should().BeEquivalentTo(new string[] { "foo3", "bar3", "baz3", "quux3" }); } + [Fact] + public async Task GetAllGrantsAsync_should_initiate_telemetry_trace() + { + Mock.Get(_telemetry).Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace); + + await _subject.GetAllGrantsAsync("123"); + + Mock.Get(_telemetry) + .Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + _subject, + "GetAllGrantsAsync")); + Mock.Get(_trace).Verify(t => t.Dispose(), Times.Once); + } + [Fact] public async Task RemoveAllGrantsAsync_should_remove_all_grants() { @@ -339,6 +362,22 @@ await _userConsent.StoreUserConsentAsync(new Consent (await _codes.GetAuthorizationCodeAsync(handle8)).Should().NotBeNull(); (await _codes.GetAuthorizationCodeAsync(handle9)).Should().NotBeNull(); } + + [Fact] + public async Task RemoveAllGrantsAsync_should_initiate_telemetry_trace() + { + Mock.Get(_telemetry).Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace); + await _subject.RemoveAllGrantsAsync("123", "client1"); + + Mock.Get(_telemetry) + .Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + _subject, + "RemoveAllGrantsAsync")); + Mock.Get(_trace).Verify(t => t.Dispose(), Times.Once); + } + [Fact] public async Task RemoveAllGrantsAsync_should_filter_on_session_id() { @@ -630,7 +669,7 @@ await _codes.StoreAuthorizationCodeAsync(new AuthorizationCode _persistentGrantSerializer.Deserialize(json); }); - _subject = new DefaultPersistedGrantService(_store, mockedSerializer, _logger); + _subject = new DefaultPersistedGrantService(_store, mockedSerializer, _telemetry, _logger); var grants = (await _subject.GetAllGrantsAsync("123")).ToList(); diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultProfileServiceTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultProfileServiceTests.cs new file mode 100644 index 000000000..e6e087567 --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultProfileServiceTests.cs @@ -0,0 +1,82 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using Moq; +using Open.IdentityServer.Models; +using Open.IdentityServer.Services; +using Open.IdentityServer.UnitTests.Common; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Services.Default; + +public class DefaultProfileServiceTests +{ + private Mock _telemetry; + private Mock _trace; + + public DefaultProfileServiceTests() + { + _telemetry = new(); + _trace = new(); + } + + private DefaultProfileService CreateSubject() + { + return new DefaultProfileService( + _telemetry.Object, + TestLogger.Create()); + } + + [Fact] + public async Task GetProfileDataAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + ClaimsPrincipal sub = new ClaimsPrincipal(new ClaimsIdentity(new Claim[] + { + new Claim("sub", "123") + }, "mock")); + Client client = new Client(); + + var subject = CreateSubject(); + + await subject.GetProfileDataAsync(new ProfileDataRequestContext + { + Client = client, + Subject = sub, + RequestedClaimTypes = Array.Empty() + }); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + subject, + "GetProfileDataAsync"), Times.Once); + _trace.Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task IsActiveAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var subject = CreateSubject(); + + ClaimsPrincipal sub = new ClaimsPrincipal(new ClaimsIdentity(new Claim[] + { + new Claim("sub", "123") + }, "mock")); + Client client = new Client(); + await subject.IsActiveAsync(new IsActiveContext(sub, client, "caller")); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + subject, + "IsActiveAsync"), Times.Once); + _trace.Verify(t => t.Dispose(), Times.Once); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultRefreshTokenServiceTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultRefreshTokenServiceTests.cs index ed9288634..62e5feb5a 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultRefreshTokenServiceTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultRefreshTokenServiceTests.cs @@ -3,7 +3,7 @@ // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. using AwesomeAssertions; -using IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer; using Open.IdentityServer.Models; using Open.IdentityServer.Services; @@ -12,10 +12,11 @@ using System; using System.Security.Claims; using System.Threading.Tasks; -using IdentityServer.UnitTests.Validation.Setup; +using Moq; +using Open.IdentityServer.UnitTests.Validation.Setup; using Xunit; -namespace IdentityServer.UnitTests.Services.Default; +namespace Open.IdentityServer.UnitTests.Services.Default; public class DefaultRefreshTokenServiceTests { @@ -24,6 +25,8 @@ public class DefaultRefreshTokenServiceTests private ClaimsPrincipal _user = new IdentityServerUser("123").CreatePrincipal(); private StubClock _clock = new StubClock(); + private Mock _telemetry = new (); + private Mock _trace = new (); public DefaultRefreshTokenServiceTests() { @@ -31,12 +34,14 @@ public DefaultRefreshTokenServiceTests() new InMemoryPersistedGrantStore(), new PersistentGrantSerializer(), new DefaultHandleGenerationService(), + new NopTelemetryService(), TestLogger.Create()); _subject = new DefaultRefreshTokenService( _store, new TestProfileService(), _clock, + _telemetry.Object, TestLogger.Create()); } @@ -508,4 +513,97 @@ public async Task ValidateRefreshToken_valid_token_should_succeed() result.IsError.Should().BeFalse(); } + + [Fact] + public async Task CreateRefreshToken_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var client = new Client(); + var accessToken = new Token(); + + await _subject.CreateRefreshTokenAsync(new RefreshTokenCreationRequest + { + Subject = _user, AccessToken = accessToken, Client = client, AuthorisedScopes = [], + }); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + _subject, + "CreateRefreshTokenAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task ValidateRefreshToken_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var client = new Client + { + ClientId = "client1", + AllowOfflineAccess = true, + RefreshTokenUsage = TokenUsage.OneTimeOnly + }; + + var refreshToken = new RefreshToken + { + CreationTime = DateTime.UtcNow, + Lifetime = 10, + Subject = new IdentityServerUser("123").CreatePrincipal(), + ClientId = client.ClientId, + AuthorizedScopes = [], + }; + + var handle = await _store.StoreRefreshTokenAsync(refreshToken); + + var now = DateTime.UtcNow; + _clock.UtcNowFunc = () => now; + + await _subject.ValidateRefreshTokenAsync(handle, client); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + _subject, + "ValidateRefreshTokenAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task UpdateRefreshToken_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var client = new Client + { + ClientId = "client1", + RefreshTokenUsage = TokenUsage.OneTimeOnly + }; + + var refreshToken = new RefreshToken + { + CreationTime = DateTime.UtcNow, + Lifetime = 10, + + Subject = new IdentityServerUser("123").CreatePrincipal(), + ClientId = client.ClientId, + AuthorizedScopes = [], + }; + + var handle = await _store.StoreRefreshTokenAsync(refreshToken); + + var now = DateTime.UtcNow; + _clock.UtcNowFunc = () => now; + + await _subject.UpdateRefreshTokenAsync(handle, refreshToken, client); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + _subject, + "UpdateRefreshTokenAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } } \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultReplayCacheTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultReplayCacheTests.cs new file mode 100644 index 000000000..4db41621a --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultReplayCacheTests.cs @@ -0,0 +1,64 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Distributed; +using Moq; +using Open.IdentityServer.Services; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Services.Default; + +public class DefaultReplayCacheTests +{ + private Mock _cache = new(); + private Mock _telemetry = new(); + private Mock _trace = new(); + + public DefaultReplayCacheTests() + { + } + + private DefaultReplayCache CreateSubject() + { + return new DefaultReplayCache(_cache.Object, _telemetry.Object); + } + + [Fact] + public async Task AddAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var subject = CreateSubject(); + + await subject.AddAsync("test-purpose", "test-handle", DateTimeOffset.UtcNow.AddMinutes(5)); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + subject, + "AddAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task ExistsAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + _cache.Setup(c => c.GetAsync(It.IsAny(), default)) + .ReturnsAsync(new byte[] { 1 }); + + var subject = CreateSubject(); + + await subject.ExistsAsync("test-purpose", "test-handle"); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + subject, + "ExistsAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultTelemetryServiceTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultTelemetryServiceTests.cs new file mode 100644 index 000000000..f35053a59 --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultTelemetryServiceTests.cs @@ -0,0 +1,711 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using AwesomeAssertions; +using Open.IdentityServer; +using Open.IdentityServer.Services; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Services.Default; + +public class DefaultTelemetryServiceTests : IDisposable +{ + private DefaultTelemetryService _subject; + private MeterProvider _meterProvider; + private ICollection _exportedMetrics; + + public DefaultTelemetryServiceTests() + { + _exportedMetrics = new List(); + + _subject = new DefaultTelemetryService(); + _meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(TelemetryConstants.MetricsConstants.MeterName) + .AddInMemoryExporter(_exportedMetrics) + .Build(); + } + + public void Dispose() + { + _meterProvider?.Dispose(); + } + + [Fact] + public void CountOperationError_WhenCalledWithNoTags_ShouldIncrementCounterWithSuccessTag() + { + const string error = "something goes bang"; + + var expectedTags = new TagList() + { + { TelemetryConstants.TagConstants.Result, TelemetryConstants.TagConstants.InternalError }, + { TelemetryConstants.TagConstants.Error, error } + }; + + _subject.CountInternalError(error); + + _meterProvider.ForceFlush(); + + Metric metric = _exportedMetrics.Single(); + + metric.Name.Should().Be(TelemetryConstants.MetricsConstants.OperationCounterName); + + var accessor = metric.GetMetricPoints(); + foreach (MetricPoint point in accessor) + { + point.GetSumLong().Should().Be(1); + + point.Tags.ShouldBeEquivalentTo(expectedTags); + } + } + + [Fact] + public void BeginActiveRequest_WhenCalled_ShouldIncrementCounterWithExpectedTags() + { + const string endpoint = "expectedEndpoint"; + const string path = "expectedPath"; + + var expectedTags = new TagList + { + { TelemetryConstants.TagConstants.Endpoint, endpoint }, + { TelemetryConstants.TagConstants.Path, path } + }; + + _subject.BeginActiveRequest(endpoint, path); + _meterProvider.ForceFlush(); + + Metric metric = _exportedMetrics.Single(); + + metric.Name.Should().Be(TelemetryConstants.MetricsConstants.ActiveRequestsCounterName); + + var accessor = metric.GetMetricPoints(); + foreach (MetricPoint point in accessor) + { + point.GetSumLong().Should().Be(1); + + point.Tags.ShouldBeEquivalentTo(expectedTags); + } + } + + [Fact] + public void BeginActiveRequest_WhenDisposed_ShouldDecrementCounterWithExpectedTags() + { + const string endpoint = "expectedEndpoint"; + const string path = "expectedPath"; + + var expectedTags = new TagList + { + { TelemetryConstants.TagConstants.Endpoint, endpoint }, + { TelemetryConstants.TagConstants.Path, path } + }; + + var disposable = _subject.BeginActiveRequest(endpoint, path); + disposable.Dispose(); + _meterProvider.ForceFlush(); + + Metric metric = _exportedMetrics.Single(); + + metric.Name.Should().Be(TelemetryConstants.MetricsConstants.ActiveRequestsCounterName); + + var accessor = metric.GetMetricPoints(); + foreach (MetricPoint point in accessor) + { + point.GetSumLong().Should().Be(0); + + point.Tags.ShouldBeEquivalentTo(expectedTags); + } + } + + [Fact] + public void CountApiSecretValidation_WhenCalledWithClientAndAuthMethod_ShouldIncrementCounterWithExpectedTags() + { + const string client = "expectedClient"; + const string authMethod = "expectedAuthMethod"; + const string expectedCounterName = TelemetryConstants.MetricsConstants.ApiSecretValidationCounterName; + + var expectedTags = new TagList + { + { TelemetryConstants.TagConstants.Api, client }, + { TelemetryConstants.TagConstants.AuthMethod, authMethod } + }; + + _subject.CountApiSecretValidation(client, authMethod); + + _meterProvider.ForceFlush(); + + Metric metric = _exportedMetrics.Single(m => m.Name == expectedCounterName); + + var accessor = metric.GetMetricPoints(); + foreach (MetricPoint point in accessor) + { + point.GetSumLong().Should().Be(1); + + point.Tags.ShouldBeEquivalentTo(expectedTags); + } + } + + [Fact] + public void CountApiSecretValidation_WhenCalledWithClientAnError_ShouldIncrementCounterWithExpectedTags() + { + const string client = "expectedClient"; + const string error = "expectedError"; + const string expectedCounterName = TelemetryConstants.MetricsConstants.ApiSecretValidationCounterName; + + var expectedTags = new TagList + { + { TelemetryConstants.TagConstants.Api, client }, + { TelemetryConstants.TagConstants.Error, error } + }; + + _subject.CountApiSecretValidation(client, error: error); + + _meterProvider.ForceFlush(); + + Metric metric = _exportedMetrics.Single(m => m.Name == expectedCounterName); + + var accessor = metric.GetMetricPoints(); + foreach (MetricPoint point in accessor) + { + point.GetSumLong().Should().Be(1); + + point.Tags.ShouldBeEquivalentTo(expectedTags); + } + } + + [Fact] + public void + CountApiSecretValidation_WhenCalledWithClientAndAuthMethod_ShouldIncrementOperationCounterWithExpectedTags() + { + const string client = "expectedClient"; + const string authMethod = "expectedAuthMethod"; + const string expectedCounterName = TelemetryConstants.MetricsConstants.OperationCounterName; + + var expectedTags = new TagList + { + { TelemetryConstants.TagConstants.Result, TelemetryConstants.TagConstants.Success }, + { TelemetryConstants.TagConstants.Client, client }, + }; + + _subject.CountApiSecretValidation(client, authMethod); + + _meterProvider.ForceFlush(); + + Metric metric = _exportedMetrics.Single(m => m.Name == expectedCounterName); + + var accessor = metric.GetMetricPoints(); + foreach (MetricPoint point in accessor) + { + point.GetSumLong().Should().Be(1); + + point.Tags.ShouldBeEquivalentTo(expectedTags); + } + } + + [Fact] + public void CountApiSecretValidation_WhenCalledWithClientAnError_ShouldIncrementOperationCounterWithExpectedTags() + { + const string client = "expectedClient"; + const string error = "expectedError"; + const string expectedCounterName = TelemetryConstants.MetricsConstants.OperationCounterName; + + var expectedTags = new TagList + { + { TelemetryConstants.TagConstants.Result, TelemetryConstants.TagConstants.Error }, + { TelemetryConstants.TagConstants.Client, client }, + { TelemetryConstants.TagConstants.Error, error } + }; + + _subject.CountApiSecretValidation(client, error: error); + + _meterProvider.ForceFlush(); + + Metric metric = _exportedMetrics.Single(m => m.Name == expectedCounterName); + + var accessor = metric.GetMetricPoints(); + foreach (MetricPoint point in accessor) + { + point.GetSumLong().Should().Be(1); + + point.Tags.ShouldBeEquivalentTo(expectedTags); + } + } + + [Theory] + [InlineData(TelemetryConstants.MetricsConstants.BackChannelAuthenticationCounterName, "clientid", null)] + [InlineData(TelemetryConstants.MetricsConstants.BackChannelAuthenticationCounterName, "clientid", "some error")] + [InlineData(TelemetryConstants.MetricsConstants.OperationCounterName, "clientid", null)] + [InlineData(TelemetryConstants.MetricsConstants.OperationCounterName, "clientid", "some error")] + public void CountBackchannelAuthentication_WhenCalled_ShouldIncrementCorrectCountersWithExpectedTags( + string counterName, + string client, + string error) + { + var expectedTags = new TagList(); + if (counterName == TelemetryConstants.MetricsConstants.OperationCounterName) + { + expectedTags.Add(TelemetryConstants.TagConstants.Result, + error == null ? TelemetryConstants.TagConstants.Success : TelemetryConstants.TagConstants.Error); + } + + expectedTags.Add(TelemetryConstants.TagConstants.Client, client); + if (error != null) expectedTags.Add(TelemetryConstants.TagConstants.Error, error); + + + _subject.CountBackchannelAuthentication(client, error: error); + + _meterProvider.ForceFlush(); + + Metric metric = _exportedMetrics.Single(m => m.Name == counterName); + + var accessor = metric.GetMetricPoints(); + foreach (MetricPoint point in accessor) + { + point.GetSumLong().Should().Be(1); + + point.Tags.ShouldBeEquivalentTo(expectedTags); + } + } + + [Theory] + [InlineData(TelemetryConstants.MetricsConstants.ClientConfigValidationCounterName, "clientid", null)] + [InlineData(TelemetryConstants.MetricsConstants.ClientConfigValidationCounterName, "clientid", "some error")] + [InlineData(TelemetryConstants.MetricsConstants.OperationCounterName, "clientid", null)] + [InlineData(TelemetryConstants.MetricsConstants.OperationCounterName, "clientid", "some error")] + public void CountClientConfigValidation_WhenCalled_ShouldIncrementCorrectCountersWithExpectedTags( + string counterName, + string client, + string error) + { + var expectedTags = new TagList(); + if (counterName == TelemetryConstants.MetricsConstants.OperationCounterName) + { + expectedTags.Add(TelemetryConstants.TagConstants.Result, + error == null ? TelemetryConstants.TagConstants.Success : TelemetryConstants.TagConstants.Error); + } + + expectedTags.Add(TelemetryConstants.TagConstants.Client, client); + if (error != null) expectedTags.Add(TelemetryConstants.TagConstants.Error, error); + + + _subject.CountClientConfigValidation(client, error: error); + + _meterProvider.ForceFlush(); + + Metric metric = _exportedMetrics.Single(m => m.Name == counterName); + + var accessor = metric.GetMetricPoints(); + foreach (MetricPoint point in accessor) + { + point.GetSumLong().Should().Be(1); + + point.Tags.ShouldBeEquivalentTo(expectedTags); + } + } + + [Theory] + [InlineData("clientid", "authMethod", null)] + [InlineData("clientid", null, "some error")] + public void CountClientSecretValidation_WhenCalled_ShouldIncrementCountersWithExpectedTags( + string client, + string authMethod, + string error) + { + const string counterName = TelemetryConstants.MetricsConstants.ClientSecretValidationCounterName; + + var expectedTags = new TagList(); + expectedTags.Add(TelemetryConstants.TagConstants.Client, client); + if (authMethod != null) expectedTags.Add(TelemetryConstants.TagConstants.AuthMethod, authMethod); + if (error != null) expectedTags.Add(TelemetryConstants.TagConstants.Error, error); + + + _subject.CountClientSecretValidation(client, authMethod, error); + + _meterProvider.ForceFlush(); + + Metric metric = _exportedMetrics.Single(m => m.Name == counterName); + + var accessor = metric.GetMetricPoints(); + foreach (MetricPoint point in accessor) + { + point.GetSumLong().Should().Be(1); + + point.Tags.ShouldBeEquivalentTo(expectedTags); + } + } + + [Theory] + [InlineData("clientid", "authMethod", null)] + [InlineData("clientid", null, "some error")] + public void CountClientSecretValidation_WhenCalled_ShouldIncrementOperationCountersWithExpectedTags( + string client, + string authMethod, + string error) + { + const string counterName = TelemetryConstants.MetricsConstants.OperationCounterName; + + var expectedTags = new TagList(); + expectedTags.Add(TelemetryConstants.TagConstants.Result, + error == null ? TelemetryConstants.TagConstants.Success : TelemetryConstants.TagConstants.Error); + expectedTags.Add(TelemetryConstants.TagConstants.Client, client); + if (error != null) expectedTags.Add(TelemetryConstants.TagConstants.Error, error); + + + _subject.CountClientSecretValidation(client, authMethod, error); + + _meterProvider.ForceFlush(); + + Metric metric = _exportedMetrics.Single(m => m.Name == counterName); + + var accessor = metric.GetMetricPoints(); + foreach (MetricPoint point in accessor) + { + point.GetSumLong().Should().Be(1); + + point.Tags.ShouldBeEquivalentTo(expectedTags); + } + } + + [Theory] + [InlineData(TelemetryConstants.MetricsConstants.ResourceOwnerAuthenticationCounterName, "clientid", null)] + [InlineData(TelemetryConstants.MetricsConstants.ResourceOwnerAuthenticationCounterName, "clientid", "some error")] + [InlineData(TelemetryConstants.MetricsConstants.OperationCounterName, "clientid", null)] + [InlineData(TelemetryConstants.MetricsConstants.OperationCounterName, "clientid", "some error")] + public void CountResourceOwnerAuthentication_WhenCalled_ShouldIncrementCorrectCountersWithExpectedTags( + string counterName, + string client, + string error) + { + var expectedTags = new TagList(); + if (counterName == TelemetryConstants.MetricsConstants.OperationCounterName) + { + expectedTags.Add(TelemetryConstants.TagConstants.Result, + error == null ? TelemetryConstants.TagConstants.Success : TelemetryConstants.TagConstants.Error); + } + + expectedTags.Add(TelemetryConstants.TagConstants.Client, client); + if (error != null) expectedTags.Add(TelemetryConstants.TagConstants.Error, error); + + + _subject.CountResourceOwnerAuthentication(client, error: error); + + _meterProvider.ForceFlush(); + + Metric metric = _exportedMetrics.Single(m => m.Name == counterName); + + var accessor = metric.GetMetricPoints(); + foreach (MetricPoint point in accessor) + { + point.GetSumLong().Should().Be(1); + + point.Tags.ShouldBeEquivalentTo(expectedTags); + } + } + + [Theory] + [InlineData(TelemetryConstants.MetricsConstants.DeviceAuthenticationCounterName, "clientid", null)] + [InlineData(TelemetryConstants.MetricsConstants.DeviceAuthenticationCounterName, "clientid", "some error")] + [InlineData(TelemetryConstants.MetricsConstants.OperationCounterName, "clientid", null)] + [InlineData(TelemetryConstants.MetricsConstants.OperationCounterName, "clientid", "some error")] + public void CountDeviceAuthentication_WhenCalled_ShouldIncrementCorrectCountersWithExpectedTags( + string counterName, + string client, + string error) + { + var expectedTags = new TagList(); + if (counterName == TelemetryConstants.MetricsConstants.OperationCounterName) + { + expectedTags.Add(TelemetryConstants.TagConstants.Result, + error == null ? TelemetryConstants.TagConstants.Success : TelemetryConstants.TagConstants.Error); + } + + expectedTags.Add(TelemetryConstants.TagConstants.Client, client); + if (error != null) expectedTags.Add(TelemetryConstants.TagConstants.Error, error); + + + _subject.CountDeviceAuthentication(client, error: error); + + _meterProvider.ForceFlush(); + + Metric metric = _exportedMetrics.Single(m => m.Name == counterName); + + var accessor = metric.GetMetricPoints(); + foreach (MetricPoint point in accessor) + { + point.GetSumLong().Should().Be(1); + + point.Tags.ShouldBeEquivalentTo(expectedTags); + } + } + + [Theory] + [InlineData("clientid", true, null)] + [InlineData("clientid", null, "some error")] + public void CountTokenIntrospection_WhenCalled_ShouldIncrementCountersWithExpectedTags( + string client, + bool? active, + string error) + { + const string counterName = TelemetryConstants.MetricsConstants.IntrospectionCounterName; + + var expectedTags = new TagList(); + expectedTags.Add(TelemetryConstants.TagConstants.Caller, client); + if (active != null) expectedTags.Add(TelemetryConstants.TagConstants.Active, active.Value); + if (error != null) expectedTags.Add(TelemetryConstants.TagConstants.Error, error); + + + _subject.CountTokenIntrospection(client, active, error); + + _meterProvider.ForceFlush(); + + Metric metric = _exportedMetrics.Single(m => m.Name == counterName); + + var accessor = metric.GetMetricPoints(); + foreach (MetricPoint point in accessor) + { + point.GetSumLong().Should().Be(1); + + point.Tags.ShouldBeEquivalentTo(expectedTags); + } + } + + [Theory] + [InlineData("clientid", false, null)] + [InlineData("clientid", null, "some error")] + public void CountTokenIntrospection_WhenCalled_ShouldIncrementOperationCountersWithExpectedTags( + string client, + bool? active, + string error) + { + const string counterName = TelemetryConstants.MetricsConstants.OperationCounterName; + + var expectedTags = new TagList(); + expectedTags.Add(TelemetryConstants.TagConstants.Result, + error == null ? TelemetryConstants.TagConstants.Success : TelemetryConstants.TagConstants.Error); + expectedTags.Add(TelemetryConstants.TagConstants.Client, client); + if (error != null) expectedTags.Add(TelemetryConstants.TagConstants.Error, error); + + + _subject.CountTokenIntrospection(client, active, error); + + _meterProvider.ForceFlush(); + + Metric metric = _exportedMetrics.Single(m => m.Name == counterName); + + var accessor = metric.GetMetricPoints(); + foreach (MetricPoint point in accessor) + { + point.GetSumLong().Should().Be(1); + + point.Tags.ShouldBeEquivalentTo(expectedTags); + } + } + + [Theory] + [InlineData(TelemetryConstants.MetricsConstants.PushedAuthorizationRequestCounterName, "clientid", null)] + [InlineData(TelemetryConstants.MetricsConstants.PushedAuthorizationRequestCounterName, "clientid", "some error")] + [InlineData(TelemetryConstants.MetricsConstants.OperationCounterName, "clientid", null)] + [InlineData(TelemetryConstants.MetricsConstants.OperationCounterName, "clientid", "some error")] + public void CountPushedAuthentication_WhenCalled_ShouldIncrementCorrectCountersWithExpectedTags( + string counterName, + string client, + string error) + { + var expectedTags = new TagList(); + if (counterName == TelemetryConstants.MetricsConstants.OperationCounterName) + { + expectedTags.Add(TelemetryConstants.TagConstants.Result, + error == null ? TelemetryConstants.TagConstants.Success : TelemetryConstants.TagConstants.Error); + } + + expectedTags.Add(TelemetryConstants.TagConstants.Client, client); + if (error != null) expectedTags.Add(TelemetryConstants.TagConstants.Error, error); + + + _subject.CountPushedAuthorizationRequest(client, error: error); + + _meterProvider.ForceFlush(); + + Metric metric = _exportedMetrics.Single(m => m.Name == counterName); + + var accessor = metric.GetMetricPoints(); + foreach (MetricPoint point in accessor) + { + point.GetSumLong().Should().Be(1); + + point.Tags.ShouldBeEquivalentTo(expectedTags); + } + } + + [Theory] + [InlineData(TelemetryConstants.MetricsConstants.RevocationCounterName, "clientid", null)] + [InlineData(TelemetryConstants.MetricsConstants.RevocationCounterName, "clientid", "some error")] + [InlineData(TelemetryConstants.MetricsConstants.OperationCounterName, "clientid", null)] + [InlineData(TelemetryConstants.MetricsConstants.OperationCounterName, "clientid", "some error")] + public void CountTokenRevocation_WhenCalled_ShouldIncrementCorrectCountersWithExpectedTags( + string counterName, + string client, + string error) + { + var expectedTags = new TagList(); + if (counterName == TelemetryConstants.MetricsConstants.OperationCounterName) + { + expectedTags.Add(TelemetryConstants.TagConstants.Result, + error == null ? TelemetryConstants.TagConstants.Success : TelemetryConstants.TagConstants.Error); + } + + expectedTags.Add(TelemetryConstants.TagConstants.Client, client); + if (error != null) expectedTags.Add(TelemetryConstants.TagConstants.Error, error); + + + _subject.CountTokenRevocation(client, error: error); + + _meterProvider.ForceFlush(); + + Metric metric = _exportedMetrics.Single(m => m.Name == counterName); + + var accessor = metric.GetMetricPoints(); + foreach (MetricPoint point in accessor) + { + point.GetSumLong().Should().Be(1); + + point.Tags.ShouldBeEquivalentTo(expectedTags); + } + } + + [Theory] + [InlineData("clientid", "grant", null)] + [InlineData("clientid", "grant", "some error")] + public void CountTokenIssued_WhenCalled_ShouldIncrementCountersWithExpectedTags( + string client, + string grantType, + string error) + { + const string counterName = TelemetryConstants.MetricsConstants.TokenIssuedCounterName; + + var expectedTags = new TagList(); + expectedTags.Add(TelemetryConstants.TagConstants.Client, client); + expectedTags.Add(TelemetryConstants.TagConstants.GrantType, grantType); + if (error != null) expectedTags.Add(TelemetryConstants.TagConstants.Error, error); + + + _subject.CountTokenIssued(client, grantType, error); + + _meterProvider.ForceFlush(); + + Metric metric = _exportedMetrics.Single(m => m.Name == counterName); + + var accessor = metric.GetMetricPoints(); + foreach (MetricPoint point in accessor) + { + point.GetSumLong().Should().Be(1); + + point.Tags.ShouldBeEquivalentTo(expectedTags); + } + } + + [Theory] + [InlineData("clientid", "grant", null)] + [InlineData("clientid", "grant", "some error")] + public void CountTokenIssued_WhenCalled_ShouldIncrementOperationCountersWithExpectedTags( + string client, + string grantType, + string error) + { + const string counterName = TelemetryConstants.MetricsConstants.OperationCounterName; + + var expectedTags = new TagList(); + expectedTags.Add(TelemetryConstants.TagConstants.Result, + error == null ? TelemetryConstants.TagConstants.Success : TelemetryConstants.TagConstants.Error); + expectedTags.Add(TelemetryConstants.TagConstants.Client, client); + if (error != null) expectedTags.Add(TelemetryConstants.TagConstants.Error, error); + + + _subject.CountTokenIssued(client, grantType, error); + + _meterProvider.ForceFlush(); + + Metric metric = _exportedMetrics.Single(m => m.Name == counterName); + + var accessor = metric.GetMetricPoints(); + foreach (MetricPoint point in accessor) + { + point.GetSumLong().Should().Be(1); + + point.Tags.ShouldBeEquivalentTo(expectedTags); + } + } + + [Theory] + [InlineData(TelemetryConstants.TraceCategories.Basic)] + [InlineData(TelemetryConstants.TraceCategories.Cache)] + [InlineData(TelemetryConstants.TraceCategories.Services)] + [InlineData(TelemetryConstants.TraceCategories.Stores)] + [InlineData(TelemetryConstants.TraceCategories.Validation)] + public void Trace_WithKnownCategory_ShouldStartAndStopActivity(string traceCategoryName) + { + var started = new List(); + var stopped = new List(); + + using var listener = new ActivityListener + { + ShouldListenTo = source => source.Name == traceCategoryName, + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData, + ActivityStarted = activity => started.Add(activity), + ActivityStopped = activity => stopped.Add(activity) + }; + + ActivitySource.AddActivityListener(listener); + + using (var trace = _subject.Trace(traceCategoryName, "test-activity")) + { + started.Should().HaveCount(1); + started[0].DisplayName.Should().Be("test-activity"); + started[0].Source.Name.Should().Be(traceCategoryName); + + stopped.Should().BeEmpty(); + } + + stopped.Should().HaveCount(1); + } + + [Fact] + public void Trace_WithUnknownCategory_ShouldReturnNull() + { + var started = new List(); + + using var listener = new ActivityListener + { + ShouldListenTo = _ => true, + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData, + ActivityStarted = activity => started.Add(activity), + }; + + ActivitySource.AddActivityListener(listener); + + using var trace = _subject.Trace("unknown-category", "test-activity"); + + trace.Should().BeNull(); + started.Should().BeEmpty(); + } +} + +public static class OpenTelemetryTestExtensions +{ + extension(OpenTelemetry.ReadOnlyTagCollection tags) + { + public void ShouldBeEquivalentTo(TagList expected) + { + tags.Count.Should().Be(expected.Count, "The collections should have the same number of elements"); + + foreach (KeyValuePair tag in tags) + { + expected.Should().Contain(tag, $"The tag '{tag.Key}' with value '{tag.Value}' should be present in the expected collection"); + } + } + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultTokenCreationServiceTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultTokenCreationServiceTests.cs new file mode 100644 index 000000000..2c82bab39 --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultTokenCreationServiceTests.cs @@ -0,0 +1,80 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Tokens; +using Moq; +using Open.IdentityServer.Configuration; +using Open.IdentityServer.Models; +using Open.IdentityServer.Services; +using Open.IdentityServer.UnitTests.Common; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Services.Default; + +public class DefaultTokenCreationServiceTests +{ + private Mock _clock; + private Mock _keys; + private IdentityServerOptions _options; + private Mock _telemetry; + private Mock _trace; + + public DefaultTokenCreationServiceTests() + { + _clock = new Mock(); + _keys = new Mock(); + _telemetry = new Mock(); + _trace = new Mock(); + _options = new IdentityServerOptions(); + + _keys + .Setup(k => k.GetSigningCredentialsAsync(It.IsAny>())) + .ReturnsAsync(new SigningCredentials( + new RsaSecurityKey(RSA.Create(2048)), + SecurityAlgorithms.RsaSha256)); + } + + private DefaultTokenCreationService CreateSubject() + { + return new DefaultTokenCreationService( + _clock.Object, + _keys.Object, + _options, + _telemetry.Object, + TestLogger.Create()); + } + + [Fact] + public async Task CreateTokenAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var now = DateTime.UtcNow; + + _clock.Setup(c => c.GetUtcNow()).Returns(now); + + var token = new Token + { + Type = OidcConstants.TokenTypes.AccessToken, + CreationTime = now, + Lifetime = 60, + Issuer = "https://issuer.test", + ClientId = "client" + }; + + var subject = CreateSubject(); + + await subject.CreateTokenAsync(token); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + subject, + "CreateTokenAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultTokenServiceTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultTokenServiceTests.cs index c723bce7b..41ef77747 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultTokenServiceTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultTokenServiceTests.cs @@ -5,29 +5,33 @@ using System.Linq; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer.Configuration; using Open.IdentityServer.Models; using Open.IdentityServer.Services; using Open.IdentityServer.Validation; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; +using Moq; using Open.IdentityServer; using Xunit; -namespace IdentityServer.UnitTests.Services.Default; +namespace Open.IdentityServer.UnitTests.Services.Default; public class DefaultTokenServiceTests { private DefaultTokenService _subject; - MockClaimsService _mockClaimsService = new MockClaimsService(); - MockReferenceTokenStore _mockReferenceTokenStore = new MockReferenceTokenStore(); - MockTokenCreationService _mockTokenCreationService = new MockTokenCreationService(); - DefaultHttpContext _httpContext = new DefaultHttpContext(); - MockSystemClock _mockSystemClock = new MockSystemClock(); - MockKeyMaterialService _mockKeyMaterialService = new MockKeyMaterialService(); - IdentityServerOptions _options = new IdentityServerOptions(); + private MockClaimsService _mockClaimsService = new MockClaimsService(); + private MockReferenceTokenStore _mockReferenceTokenStore = new MockReferenceTokenStore(); + private MockTokenCreationService _mockTokenCreationService = new MockTokenCreationService(); + private DefaultHttpContext _httpContext = new DefaultHttpContext(); + private MockSystemClock _mockSystemClock = new MockSystemClock(); + private MockKeyMaterialService _mockKeyMaterialService = new MockKeyMaterialService(); + private IdentityServerOptions _options = new IdentityServerOptions(); + private Mock _telemetry = new(); + private Mock _trace = new(); public DefaultTokenServiceTests() { @@ -45,6 +49,7 @@ public DefaultTokenServiceTests() _mockSystemClock, _mockKeyMaterialService, _options, + _telemetry.Object, TestLogger.Create()); } @@ -170,6 +175,7 @@ public async Task CreateAccessTokenAsync_when_no_session_should_not_include_sid( result.Claims.SingleOrDefault(x => x.Type == JwtClaimTypes.SessionId).Should().BeNull(); } + [Fact] public async Task CreateAccessTokenAsync_when_session_should_include_sid() { @@ -187,4 +193,75 @@ public async Task CreateAccessTokenAsync_when_session_should_include_sid() result.Claims.SingleOrDefault(x => x.Type == JwtClaimTypes.SessionId).Value.Should().Be("123"); } + + [Fact] + public async Task CreateIdentityTokenAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + _mockKeyMaterialService.SigningCredentials.Add( + new SigningCredentials(CryptoHelper.CreateRsaSecurityKey(), "RS256")); + + var request = new TokenCreationRequest + { + ValidatedResources = new ResourceValidationResult(), + ValidatedRequest = new ValidatedRequest() + { + Client = new Client { }, + } + }; + + await _subject.CreateIdentityTokenAsync(request); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + _subject, + "CreateIdentityTokenAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task CreateAccessTokenAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var request = new TokenCreationRequest + { + ValidatedResources = new ResourceValidationResult(), + ValidatedRequest = new ValidatedRequest() + { + Client = new Client { }, + } + }; + + await _subject.CreateAccessTokenAsync(request); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + _subject, + "CreateAccessTokenAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task CreateSecurityTokenAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var token = new Token(OidcConstants.TokenTypes.AccessToken) + { + AccessTokenType = AccessTokenType.Jwt + }; + + await _subject.CreateSecurityTokenAsync(token); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + _subject, + "CreateSecurityTokenAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } } \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultUserSessionTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultUserSessionTests.cs index 3774c40bc..324872ba2 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultUserSessionTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DefaultUserSessionTests.cs @@ -9,7 +9,7 @@ using System.Security.Claims; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer; using Open.IdentityServer.Configuration; using Open.IdentityServer.Extensions; @@ -17,7 +17,7 @@ using Microsoft.AspNetCore.Authentication; using Xunit; -namespace IdentityServer.UnitTests.Services.Default; +namespace Open.IdentityServer.UnitTests.Services.Default; [SuppressMessage( "Usage", diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DistributedDeviceFlowThrottlingServiceTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DistributedDeviceFlowThrottlingServiceTests.cs index c12998775..3f84c6044 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DistributedDeviceFlowThrottlingServiceTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/DistributedDeviceFlowThrottlingServiceTests.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. using System; @@ -7,14 +8,15 @@ using System.Threading; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer.Configuration; using Open.IdentityServer.Models; using Open.IdentityServer.Services; using Microsoft.Extensions.Caching.Distributed; +using Moq; using Xunit; -namespace IdentityServer.UnitTests.Services.Default; +namespace Open.IdentityServer.UnitTests.Services.Default; public class DistributedDeviceFlowThrottlingServiceTests { @@ -30,11 +32,14 @@ public class DistributedDeviceFlowThrottlingServiceTests private const string CacheKey = "devicecode_"; private readonly DateTime testDate = new(2018, 06, 28, 13, 37, 42); + private Mock _telemetry = new(); + private Mock _trace = new(); + [Fact] public async Task First_Poll() { var handle = Guid.NewGuid().ToString(); - var service = new DistributedDeviceFlowThrottlingService(cache, new StubClock {UtcNowFunc = () => testDate}, options); + var service = CreateSubject(); var result = await service.ShouldSlowDown(handle, deviceCode); @@ -43,11 +48,38 @@ public async Task First_Poll() CheckCacheEntry(handle); } + private DistributedDeviceFlowThrottlingService CreateSubject() + { + return new DistributedDeviceFlowThrottlingService( + cache, + new StubClock {UtcNowFunc = () => testDate}, + options, + _telemetry.Object); + } + + [Fact] + public async Task ShouldSlowDown_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var handle = Guid.NewGuid().ToString(); + var service = CreateSubject(); + + await service.ShouldSlowDown(handle, deviceCode); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + service, + "ShouldSlowDown")); + _trace.Verify(t => t.Dispose(), Times.Once); + } + [Fact] public async Task Second_Poll_Too_Fast() { var handle = Guid.NewGuid().ToString(); - var service = new DistributedDeviceFlowThrottlingService(cache, new StubClock { UtcNowFunc = () => testDate }, options); + var service = CreateSubject(); await cache.SetAsync( CacheKey + handle, @@ -66,7 +98,7 @@ public async Task Second_Poll_After_Interval() { var handle = Guid.NewGuid().ToString(); - var service = new DistributedDeviceFlowThrottlingService(cache, new StubClock { UtcNowFunc = () => testDate }, options); + var service = CreateSubject(); await cache.SetAsync( $"devicecode_{handle}", @@ -89,7 +121,7 @@ public async Task Expired_Device_Code_Should_Not_Have_Expiry_in_Past() var handle = Guid.NewGuid().ToString(); deviceCode.CreationTime = testDate.AddSeconds(-deviceCode.Lifetime * 2); - var service = new DistributedDeviceFlowThrottlingService(cache, new StubClock { UtcNowFunc = () => testDate }, options); + var service = CreateSubject(); var result = await service.ShouldSlowDown(handle, deviceCode); diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/LogoutNotificationServiceTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/LogoutNotificationServiceTests.cs new file mode 100644 index 000000000..17dae364e --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/LogoutNotificationServiceTests.cs @@ -0,0 +1,81 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Moq; +using Open.IdentityServer.Models; +using Open.IdentityServer.Services; +using Open.IdentityServer.Stores; +using Open.IdentityServer.UnitTests.Common; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Services.Default; + +public class LogoutNotificationServiceTests +{ + private Mock _clientStore; + private Mock _httpContextAccessor; + private Mock _telemetry; + private Mock _trace; + + public LogoutNotificationServiceTests() + { + _clientStore = new Mock(); + _httpContextAccessor = new Mock(); + _telemetry = new Mock(); + _trace = new Mock(); + } + + private LogoutNotificationService CreateSubject() + { + return new LogoutNotificationService( + _clientStore.Object, + _httpContextAccessor.Object, + _telemetry.Object, + TestLogger.Create() + ); + } + + [Fact] + public async Task GetFrontChannelLogoutNotificationsUrlsAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var subject = CreateSubject(); + + await subject.GetFrontChannelLogoutNotificationsUrlsAsync(new LogoutNotificationContext + { + ClientIds = new[] { "client1", "client2" }, + SessionId = "session1" + }); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + subject, + "GetFrontChannelLogoutNotificationsUrlsAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task GetBackChannelLogoutNotificationsAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var subject = CreateSubject(); + + await subject.GetBackChannelLogoutNotificationsAsync(new LogoutNotificationContext + { + ClientIds = new[] { "client1", "client2" }, + SessionId = "session1" + }); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + subject, + "GetBackChannelLogoutNotificationsAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/NumericUserCodeServiceTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/NumericUserCodeServiceTests.cs index edfb195bf..52df4b188 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/NumericUserCodeServiceTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/NumericUserCodeServiceTests.cs @@ -6,7 +6,7 @@ using Open.IdentityServer.Services; using Xunit; -namespace IdentityServer.UnitTests.Services.Default; +namespace Open.IdentityServer.UnitTests.Services.Default; public class NumericUserCodeGeneratorTests { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/OidcReturnUrlParserTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/OidcReturnUrlParserTests.cs index 2b803eb10..1bc4186c1 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/OidcReturnUrlParserTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/OidcReturnUrlParserTests.cs @@ -19,7 +19,7 @@ using Open.IdentityServer.Validation; using Xunit; -namespace IdentityServer.UnitTests.Services.Default; +namespace Open.IdentityServer.UnitTests.Services.Default; public class OidcReturnUrlParserTests { @@ -27,6 +27,8 @@ public class OidcReturnUrlParserTests private readonly IUserSession userSession = Mock.Of(); private readonly ILogger logger = NullLogger.Instance; private IAuthorizationParametersMessageStore? authorizationParametersMessageStore = Mock.Of(); + private ITelemetryService _telemetry = Mock.Of(); + private ITrace _trace = Mock.Of(); private static readonly List claims = [ @@ -44,7 +46,7 @@ public OidcReturnUrlParserTests() .ReturnsAsync(fakeUser); } - private OidcReturnUrlParser CreateSut() => new(authorizeRequestValidator, userSession, logger, authorizationParametersMessageStore); + private OidcReturnUrlParser CreateSut() => new(authorizeRequestValidator, userSession, logger, _telemetry, authorizationParametersMessageStore); [Theory] [InlineData($"/some/path/{Constants.ProtocolRoutePaths.Authorize}", true)] @@ -198,6 +200,43 @@ public async Task ParseAsync_WhenAuthParamStoreNotNull_AndHasMessageIdParam_Shou parsedParams.GetValues("paramA").Should().BeEquivalentTo("someValue"); parsedParams.GetValues("paramB").Should().BeEquivalentTo("acmeOne", "otherVal"); } + + [Fact] + public async Task ParseAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + int stackDepth = 0; + Mock.Get(_telemetry).Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(() => stackDepth++ == 0 ? _trace : Mock.Of()); + + var subject = CreateSut(); + + await subject.ParseAsync("anything"); + + Mock.Get(_telemetry) + .Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + subject, + "ParseAsync")); + Mock.Get(_trace).Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public void IsValidReturnUrl_WhenCalled_ShouldInitiateTelemetryTrace() + { + Mock.Get(_telemetry).Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace); + + var subject = CreateSut(); + + subject.IsValidReturnUrl("anything"); + + Mock.Get(_telemetry) + .Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + subject, + "IsValidReturnUrl")); + Mock.Get(_trace).Verify(t => t.Dispose(), Times.Once); + } public bool CompareNameValueCollections(NameValueCollection nvc1, NameValueCollection nvc2) diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/ReturnUrlParserTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/ReturnUrlParserTests.cs new file mode 100644 index 000000000..e68bb2349 --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/Default/ReturnUrlParserTests.cs @@ -0,0 +1,63 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Moq; +using Open.IdentityServer.Services; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Services.Default; + +public class ReturnUrlParserTests +{ + private List _parsers; + private Mock _telemetry; + private Mock _trace; + + public ReturnUrlParserTests() + { + _parsers = new List(); + _telemetry = new Mock(); + _trace = new Mock(); + } + + private ReturnUrlParser CreateSubject() + { + return new ReturnUrlParser(_parsers, _telemetry.Object); + } + + [Fact] + public async Task ParseAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var subject = CreateSubject(); + + await subject.ParseAsync("http://localhost/"); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + subject, + "ParseAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public void IsValidReturnUrl_WhenCalled_ShouldInitiateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var subject = CreateSubject(); + + subject.IsValidReturnUrl("http://localhost/"); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + subject, + "IsValidReturnUrl")); + _trace.Verify(t => t.Dispose(), Times.Once); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/InMemory/InMemoryCorsPolicyService.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/InMemory/InMemoryCorsPolicyService.cs index c88977225..fa32d5365 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/InMemory/InMemoryCorsPolicyService.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Services/InMemory/InMemoryCorsPolicyService.cs @@ -1,16 +1,18 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. using System.Collections.Generic; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; +using Moq; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer.Models; using Open.IdentityServer.Services; using Xunit; -namespace IdentityServer.UnitTests.Services.InMemory; +namespace Open.IdentityServer.UnitTests.Services.InMemory; public class InMemoryCorsPolicyServiceTests { @@ -18,10 +20,39 @@ public class InMemoryCorsPolicyServiceTests private InMemoryCorsPolicyService _subject; private List _clients = new List(); + private Mock _telemetry = new(); + private Mock _trace = new(); public InMemoryCorsPolicyServiceTests() { - _subject = new InMemoryCorsPolicyService(TestLogger.Create(), _clients); + _subject = new InMemoryCorsPolicyService( + TestLogger.Create(), + _clients, + _telemetry.Object); + } + + [Fact] + [Trait("Category", Category)] + public async Task is_origin_allowed_should_initiate_telemetry_trace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + _clients.Add(new Client + { + AllowedCorsOrigins = new List + { + "http://foo" + } + }); + + await _subject.IsOriginAllowedAsync("http://foo"); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Services, + _subject, + "IsOriginAllowedAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); } [Fact] diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Caching/CachingClientStoreTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Caching/CachingClientStoreTests.cs new file mode 100644 index 000000000..85c2231d3 --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Caching/CachingClientStoreTests.cs @@ -0,0 +1,104 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System; +using System.Threading.Tasks; +using AwesomeAssertions; +using Moq; +using Open.IdentityServer.Configuration; +using Open.IdentityServer.Models; +using Open.IdentityServer.Services; +using Open.IdentityServer.Stores; +using Open.IdentityServer.UnitTests.Common; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Stores.Caching; + +public class CachingClientStoreTests +{ + private IdentityServerOptions _options; + private Mock _inner; + private Mock> _cache; + private Mock _telemetry; + private Mock _trace; + + private TimeSpan expiration = TimeSpan.FromMinutes(5); + + public CachingClientStoreTests() + { + _options = new IdentityServerOptions + { + Caching = new CachingOptions + { + ClientStoreExpiration = expiration + } + }; + + _inner = new Mock(); + _cache = new Mock>(); + _telemetry = new Mock(); + _trace = new Mock(); + } + + private CachingClientStore CreateSubject() + { + return new CachingClientStore( + _options, + _inner.Object, + _cache.Object, + _telemetry.Object, + TestLogger.Create>()); + } + + [Fact] + public async Task FindClientById_OnCacheMiss_ShouldStoreAndReturnClient() + { + var clientId = "client"; + var expectedClient = new Client { ClientId = clientId }; + + _cache.Setup(x => x.GetAsync(clientId)) + .ReturnsAsync((Client)null); + + _inner.Setup(x => x.FindClientByIdAsync(clientId)) + .ReturnsAsync(expectedClient); + + _cache.Setup(x => x.SetAsync(clientId, expectedClient, expiration)) + .Returns(Task.CompletedTask); + + var subject = CreateSubject(); + + var result = await subject.FindClientByIdAsync(clientId); + + result.Should().BeSameAs(expectedClient); + _cache.Verify(x => x.GetAsync(clientId), Times.Once); + _inner.Verify(x => x.FindClientByIdAsync(clientId), Times.Once); + _cache.Verify(x => x.SetAsync(clientId, expectedClient, expiration), Times.Once); + } + + [Fact] + public async Task FindClientById_WhenCalled_ShouldTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var clientId = "client"; + var expectedClient = new Client { ClientId = clientId }; + + _cache.Setup(x => x.GetAsync(clientId)) + .ReturnsAsync((Client)null); + + _inner.Setup(x => x.FindClientByIdAsync(clientId)) + .ReturnsAsync(expectedClient); + + _cache.Setup(x => x.SetAsync(clientId, expectedClient, expiration)) + .Returns(Task.CompletedTask); + + var subject = CreateSubject(); + + await subject.FindClientByIdAsync(clientId); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Cache, subject, "FindClientByIdAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Caching/CachingResourceStoreTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Caching/CachingResourceStoreTests.cs new file mode 100644 index 000000000..7e42c5f0b --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Caching/CachingResourceStoreTests.cs @@ -0,0 +1,248 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using AwesomeAssertions; +using Moq; +using Open.IdentityServer.Configuration; +using Open.IdentityServer.Models; +using Open.IdentityServer.Services; +using Open.IdentityServer.Stores; +using Open.IdentityServer.UnitTests.Common; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Stores.Caching; + +public class CachingResourceStoreTests +{ + private readonly TimeSpan _expiration = TimeSpan.FromMinutes(5); + + private readonly IdentityServerOptions _options; + private readonly Mock _inner = new(); + private readonly Mock>> _identityCache = new(); + private readonly Mock>> _apiByScopeCache = new(); + private readonly Mock>> _apiResourceCache = new(); + private readonly Mock>> _apiScopeCache = new(); + private readonly Mock> _allCache = new(); + private readonly Mock _telemetry = new(); + private readonly Mock _trace = new(); + + public CachingResourceStoreTests() + { + _options = new IdentityServerOptions + { + Caching = new CachingOptions { ResourceStoreExpiration = _expiration } + }; + } + + private CachingResourceStore CreateSubject() => + new(_options, _inner.Object, + _identityCache.Object, + _apiByScopeCache.Object, + _apiResourceCache.Object, + _apiScopeCache.Object, + _allCache.Object, + _telemetry.Object, + TestLogger.Create>()); + + [Fact] + public async Task GetAllResources_OnCacheMiss_ShouldCacheAndReturnResources() + { + var expected = new Resources(); + _allCache.Setup(x => x.GetAsync("__all__")).ReturnsAsync((Resources)null); + _inner.Setup(x => x.GetAllResourcesAsync()).ReturnsAsync(expected); + _allCache.Setup(x => x.SetAsync("__all__", expected, _expiration)).Returns(Task.CompletedTask); + + var result = await CreateSubject().GetAllResourcesAsync(); + + result.Should().BeSameAs(expected); + _allCache.Verify(x => x.GetAsync("__all__"), Times.Once); + _inner.Verify(x => x.GetAllResourcesAsync(), Times.Once); + _allCache.Verify(x => x.SetAsync("__all__", expected, _expiration), Times.Once); + } + + [Fact] + public async Task FindApiResourcesByName_OnCacheMiss_ShouldCacheAndReturnApiResources() + { + var names = new[] { "resource2", "resource1" }; + var cacheKey = "resource1,resource2"; + var expected = new List { new("resource1"), new("resource2") }; + + _apiResourceCache.Setup(x => x.GetAsync(cacheKey)).ReturnsAsync((IEnumerable)null); + _inner.Setup(x => x.FindApiResourcesByNameAsync(names)).ReturnsAsync(expected); + _apiResourceCache.Setup(x => x.SetAsync(cacheKey, expected, _expiration)).Returns(Task.CompletedTask); + + var result = await CreateSubject().FindApiResourcesByNameAsync(names); + + result.Should().BeSameAs(expected); + _apiResourceCache.Verify(x => x.GetAsync(cacheKey), Times.Once); + _inner.Verify(x => x.FindApiResourcesByNameAsync(names), Times.Once); + _apiResourceCache.Verify(x => x.SetAsync(cacheKey, expected, _expiration), Times.Once); + } + + [Fact] + public async Task FindIdentityResourcesByName_OnCacheMiss_ShouldCacheAndReturnIdentityResources() + { + var names = new[] { "profile", "openid" }; + var cacheKey = "openid,profile"; + var expected = new List { new() { Name = "openid" }, new() { Name = "profile" } }; + + _identityCache.Setup(x => x.GetAsync(cacheKey)).ReturnsAsync((IEnumerable)null); + _inner.Setup(x => x.FindIdentityResourcesByScopeNameAsync(names)).ReturnsAsync(expected); + _identityCache.Setup(x => x.SetAsync(cacheKey, expected, _expiration)).Returns(Task.CompletedTask); + + var result = await CreateSubject().FindIdentityResourcesByScopeNameAsync(names); + + result.Should().BeSameAs(expected); + _identityCache.Verify(x => x.GetAsync(cacheKey), Times.Once); + _inner.Verify(x => x.FindIdentityResourcesByScopeNameAsync(names), Times.Once); + _identityCache.Verify(x => x.SetAsync(cacheKey, expected, _expiration), Times.Once); + } + + [Fact] + public async Task FindApiResourcesByScopeName_OnCacheMiss_ShouldCacheAndReturnApiResources() + { + var names = new[] { "scope2", "scope1" }; + var cacheKey = "scope1,scope2"; + var expected = new List { new("api1"), new("api2") }; + + _apiByScopeCache.Setup(x => x.GetAsync(cacheKey)).ReturnsAsync((IEnumerable)null); + _inner.Setup(x => x.FindApiResourcesByScopeNameAsync(names)).ReturnsAsync(expected); + _apiByScopeCache.Setup(x => x.SetAsync(cacheKey, expected, _expiration)).Returns(Task.CompletedTask); + + var result = await CreateSubject().FindApiResourcesByScopeNameAsync(names); + + result.Should().BeSameAs(expected); + _apiByScopeCache.Verify(x => x.GetAsync(cacheKey), Times.Once); + _inner.Verify(x => x.FindApiResourcesByScopeNameAsync(names), Times.Once); + _apiByScopeCache.Verify(x => x.SetAsync(cacheKey, expected, _expiration), Times.Once); + } + + [Fact] + public async Task FindApiScopesByName_OnCacheMiss_ShouldCacheAndReturnApiScopes() + { + var names = new[] { "scopeB", "scopeA" }; + var cacheKey = "scopeA,scopeB"; + var expected = new List { new("scopeA"), new("scopeB") }; + + _apiScopeCache.Setup(x => x.GetAsync(cacheKey)).ReturnsAsync((IEnumerable)null); + _inner.Setup(x => x.FindApiScopesByNameAsync(names)).ReturnsAsync(expected); + _apiScopeCache.Setup(x => x.SetAsync(cacheKey, expected, _expiration)).Returns(Task.CompletedTask); + + var result = await CreateSubject().FindApiScopesByNameAsync(names); + + result.Should().BeSameAs(expected); + _apiScopeCache.Verify(x => x.GetAsync(cacheKey), Times.Once); + _inner.Verify(x => x.FindApiScopesByNameAsync(names), Times.Once); + _apiScopeCache.Verify(x => x.SetAsync(cacheKey, expected, _expiration), Times.Once); + } + + [Fact] + public async Task GetAllResources_WhenCalled_ShouldTelemetryTrace() + { + var expected = new Resources(); + _allCache.Setup(x => x.GetAsync("__all__")).ReturnsAsync((Resources)null); + _inner.Setup(x => x.GetAllResourcesAsync()).ReturnsAsync(expected); + _allCache.Setup(x => x.SetAsync("__all__", expected, _expiration)).Returns(Task.CompletedTask); + + var subject = CreateSubject(); + + await subject.GetAllResourcesAsync(); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Cache, subject, "GetAllResourcesAsync")); + } + + [Fact] + public async Task FindApiResourcesByName_WhenCalled_ShouldTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + var names = new[] { "resource2", "resource1" }; + var cacheKey = "resource1,resource2"; + var expected = new List { new("resource1"), new("resource2") }; + + _apiResourceCache.Setup(x => x.GetAsync(cacheKey)).ReturnsAsync((IEnumerable)null); + _inner.Setup(x => x.FindApiResourcesByNameAsync(names)).ReturnsAsync(expected); + _apiResourceCache.Setup(x => x.SetAsync(cacheKey, expected, _expiration)).Returns(Task.CompletedTask); + + var subject = CreateSubject(); + + await subject.FindApiResourcesByNameAsync(names); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Cache, subject, "FindApiResourcesByNameAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task FindIdentityResourcesByName_WhenCalled_ShouldTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var names = new[] { "profile", "openid" }; + var cacheKey = "openid,profile"; + var expected = new List { new() { Name = "openid" }, new() { Name = "profile" } }; + + _identityCache.Setup(x => x.GetAsync(cacheKey)).ReturnsAsync((IEnumerable)null); + _inner.Setup(x => x.FindIdentityResourcesByScopeNameAsync(names)).ReturnsAsync(expected); + _identityCache.Setup(x => x.SetAsync(cacheKey, expected, _expiration)).Returns(Task.CompletedTask); + + var subject = CreateSubject(); + + await subject.FindIdentityResourcesByScopeNameAsync(names); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Cache, subject, "FindIdentityResourcesByScopeNameAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task FindApiResourcesByScopeName_WhenCalled_ShouldTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var names = new[] { "scope2", "scope1" }; + var cacheKey = "scope1,scope2"; + var expected = new List { new("api1"), new("api2") }; + + _apiByScopeCache.Setup(x => x.GetAsync(cacheKey)).ReturnsAsync((IEnumerable)null); + _inner.Setup(x => x.FindApiResourcesByScopeNameAsync(names)).ReturnsAsync(expected); + _apiByScopeCache.Setup(x => x.SetAsync(cacheKey, expected, _expiration)).Returns(Task.CompletedTask); + + var subject = CreateSubject(); + + await subject.FindApiResourcesByScopeNameAsync(names); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Cache, subject, "FindApiResourcesByScopeNameAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task FindApiScopesByName_WhenCalled_ShouldTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var names = new[] { "scopeB", "scopeA" }; + var cacheKey = "scopeA,scopeB"; + var expected = new List { new("scopeA"), new("scopeB") }; + + _apiScopeCache.Setup(x => x.GetAsync(cacheKey)).ReturnsAsync((IEnumerable)null); + _inner.Setup(x => x.FindApiScopesByNameAsync(names)).ReturnsAsync(expected); + _apiScopeCache.Setup(x => x.SetAsync(cacheKey, expected, _expiration)).Returns(Task.CompletedTask); + + var subject = CreateSubject(); + + await subject.FindApiScopesByNameAsync(names); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Cache, subject, "FindApiScopesByNameAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/FakeKeyData.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/FakeKeyData.cs index 776cac4b7..eef6f430b 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/FakeKeyData.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/FakeKeyData.cs @@ -8,7 +8,7 @@ using Microsoft.IdentityModel.Tokens; using Open.IdentityServer.DataProtection; -namespace IdentityServer.UnitTests.Stores.Compatibility; +namespace Open.IdentityServer.UnitTests.Stores.Compatibility; public static class FakeKeyData { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerSigningCredentialStoreTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerSigningCredentialStoreTests.cs index f70e48ccb..b43068f67 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerSigningCredentialStoreTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerSigningCredentialStoreTests.cs @@ -15,11 +15,12 @@ using Open.IdentityServer.Configuration; using Open.IdentityServer.DataProtection; using Open.IdentityServer.Models; +using Open.IdentityServer.Services; using Open.IdentityServer.Stores; using Open.IdentityServer.UnitTests; using Xunit; -namespace IdentityServer.UnitTests.Stores.Compatibility; +namespace Open.IdentityServer.UnitTests.Stores.Compatibility; public class IdentityServerSigningCredentialStoreTests { @@ -29,6 +30,8 @@ public class IdentityServerSigningCredentialStoreTests private readonly DataProtectedIdentityServerKeyMaterialConverter dataProtectedIdentityServerKeyMaterialConverter; private readonly FakeTimeProvider timeProvider = new(); private readonly CompatibilityKeyStoreOptions fakeOptions = new(); + private readonly ITelemetryService telemetry = Mock.Of(); + private readonly ITrace trace = Mock.Of(); private readonly DateTime fakeNow = new(2026, 05, 01, 12, 00, 00, DateTimeKind.Utc); @@ -49,7 +52,12 @@ public IdentityServerSigningCredentialStoreTests() dataProtectedIdentityServerKeyMaterialConverter = new DataProtectedIdentityServerKeyMaterialConverter(dataProtectionProvider); } - private IdentityServerSigningCredentialStore CreateSut() => new(identityServerKeyStore, dataProtectedIdentityServerKeyMaterialConverter, timeProvider, fakeOptions); + private IdentityServerSigningCredentialStore CreateSut() => new( + identityServerKeyStore, + dataProtectedIdentityServerKeyMaterialConverter, + timeProvider, + fakeOptions, + telemetry); [Fact] public async Task GetSigningCredentialsAsync_WhenUnspecifiedDateTime_ShouldTreatAsUtcTime() @@ -414,4 +422,21 @@ public async Task GetSigningCredentialsAsync_WhenTableIsEmpty_ShouldReturnNull() actual.Should().BeNull(); } + + [Fact] + public async Task GetSigningCredentialsAsync_WhenCalled_ShouldTelemetryTrace() + { + Mock.Get(telemetry).Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(trace); + + var sut = CreateSut(); + + await sut.GetSigningCredentialsAsync(); + + // Verify that the telemetry trace was called + Mock.Get(telemetry) + .Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Stores, sut, "GetSigningCredentialsAsync")); + Mock.Get(trace).Verify(t => t.Dispose(), Times.Once); + } } \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerValidationKeysStoreTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerValidationKeysStoreTests.cs index c5238047c..af18f1543 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerValidationKeysStoreTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Compatibility/IdentityServerValidationKeysStoreTests.cs @@ -13,11 +13,12 @@ using Open.IdentityServer.Configuration; using Open.IdentityServer.DataProtection; using Open.IdentityServer.Models; +using Open.IdentityServer.Services; using Open.IdentityServer.Stores; using Open.IdentityServer.UnitTests; using Xunit; -namespace IdentityServer.UnitTests.Stores.Compatibility; +namespace Open.IdentityServer.UnitTests.Stores.Compatibility; public class IdentityServerValidationKeysStoreTests { @@ -27,6 +28,8 @@ public class IdentityServerValidationKeysStoreTests private readonly DataProtectedIdentityServerKeyMaterialConverter dataProtectedIdentityServerKeyMaterialConverter; private readonly FakeTimeProvider timeProvider = new(); private readonly CompatibilityKeyStoreOptions fakeOptions = new(); + private readonly ITelemetryService telemetry = Mock.Of(); + private readonly ITrace trace = Mock.Of(); private readonly DateTime fakeNow = new(2026, 05, 01, 12, 00, 00, DateTimeKind.Utc); @@ -47,7 +50,12 @@ public IdentityServerValidationKeysStoreTests() .Returns(dataProtector); } - private IdentityServerValidationKeysStore CreateSut() => new(identityServerKeyStore, dataProtectedIdentityServerKeyMaterialConverter, timeProvider, fakeOptions); + private IdentityServerValidationKeysStore CreateSut() => new( + identityServerKeyStore, + dataProtectedIdentityServerKeyMaterialConverter, + timeProvider, + fakeOptions, + telemetry); [Fact] public async Task GetValidationKeysAsync_WhenUnspecifiedDateTime_ShouldTreatAsUtcTime() @@ -340,4 +348,23 @@ public async Task GetValidationKeysAsync_WhenTableIsEmpty_ShouldReturnEmptyList( actual.Should().BeEmpty(); } + + + + [Fact] + public async Task GetValidationKeysAsync_WhenCalled_ShouldTelemetryTrace() + { + Mock.Get(telemetry).Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(trace); + + var sut = CreateSut(); + + await sut.GetValidationKeysAsync(); + + // Verify that the telemetry trace was called + Mock.Get(telemetry) + .Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Stores, sut, "GetValidationKeysAsync")); + Mock.Get(trace).Verify(t => t.Dispose(), Times.Once); + } } \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Default/DefaultAuthorizationCodeStoreTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Default/DefaultAuthorizationCodeStoreTests.cs new file mode 100644 index 000000000..eac51a98c --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Default/DefaultAuthorizationCodeStoreTests.cs @@ -0,0 +1,198 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using AwesomeAssertions; +using Microsoft.Extensions.Logging; +using Moq; +using Open.IdentityServer.Models; +using Open.IdentityServer.Services; +using Open.IdentityServer.Stores; +using Open.IdentityServer.Stores.Serialization; +using Open.IdentityServer.UnitTests.Common; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Stores.Default; + +public class DefaultAuthorizationCodeStoreTests +{ + private readonly IPersistedGrantStore _store = Mock.Of(); + private readonly IPersistentGrantSerializer _serializer = new PersistentGrantSerializer(); + private readonly IHandleGenerationService _handleGenerationService = Mock.Of(); + private readonly ITelemetryService _telemetry = Mock.Of(); + private readonly ITrace _trace = Mock.Of(); + + private readonly ILogger + _logger = TestLogger.Create(); + + private DefaultAuthorizationCodeStore CreateSut() => + new DefaultAuthorizationCodeStore(_store, _serializer, _handleGenerationService, _telemetry, _logger); + + [Fact] + public async Task StoreAuthorizationCodeAsync_WhenCalled_ShouldPersistGrantAndReturnHandle() + { + const string baseHandle = "test_base_handle"; + var expectedHandle = baseHandle + DefaultAuthorizationCodeStore.HexEncodingSuffix; + + Mock.Get(_handleGenerationService) + .Setup(x => x.GenerateAsync()) + .ReturnsAsync(baseHandle); + + Mock.Get(_store) + .Setup(x => x.StoreAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + var code = CreateAuthorizationCode(); + var sut = CreateSut(); + + var handle = await sut.StoreAuthorizationCodeAsync(code); + + handle.Should().Be(expectedHandle); + Mock.Get(_store).Verify(x => x.StoreAsync(It.Is(g => + g.Key == GetHashedKey(expectedHandle) && + g.Type == IdentityServerConstants.PersistedGrantTypes.AuthorizationCode && + g.ClientId == code.ClientId && + g.SessionId == code.SessionId + )), Times.Once); + } + + [Fact] + public async Task GetAuthorizationCodeAsync_WhenGrantExists_ShouldReturnDeserializedAuthorizationCode() + { + const string handle = "test_handle-1"; + var code = CreateAuthorizationCode(); + + var grant = new PersistedGrant + { + Key = GetHashedKey(handle), + Type = IdentityServerConstants.PersistedGrantTypes.AuthorizationCode, + ClientId = code.ClientId, + Data = _serializer.Serialize(code), + }; + + Mock.Get(_store) + .Setup(x => x.GetAsync(GetHashedKey(handle))) + .ReturnsAsync(grant); + + var sut = CreateSut(); + + var result = await sut.GetAuthorizationCodeAsync(handle); + + result.Should().NotBeNull(); + result.ClientId.Should().Be(code.ClientId); + result.SessionId.Should().Be(code.SessionId); + result.Lifetime.Should().Be(code.Lifetime); + } + + [Fact] + public async Task RemoveAuthorizationCodeAsync_WhenCalled_ShouldRemoveGrantByHashedKey() + { + const string handle = "test_handle-1"; + + Mock.Get(_store) + .Setup(x => x.RemoveAsync(GetHashedKey(handle))) + .Returns(Task.CompletedTask); + + var sut = CreateSut(); + + await sut.RemoveAuthorizationCodeAsync(handle); + + Mock.Get(_store).Verify(x => x.RemoveAsync(GetHashedKey(handle)), Times.Once); + } + + private static AuthorizationCode CreateAuthorizationCode() => new() + { + ClientId = "test-client", + Subject = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim("sub", "user-123") }, "test")), + SessionId = "session-abc", + Description = "test description", + CreationTime = DateTime.UtcNow, + Lifetime = 300, + }; + + private static string GetHashedKey(string handle) => + $"{handle}:{IdentityServerConstants.PersistedGrantTypes.AuthorizationCode}" + .Sha256(handle.EndsWith(DefaultAuthorizationCodeStore.HexEncodingSuffix)); + + [Fact] + public async Task StoreAuthorizationCodeAsync_WhenCalled_ShouldTelemetryTrace() + { + Mock.Get(_telemetry).Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace); + + const string baseHandle = "test_base_handle"; + var expectedHandle = baseHandle + DefaultAuthorizationCodeStore.HexEncodingSuffix; + + Mock.Get(_handleGenerationService) + .Setup(x => x.GenerateAsync()) + .ReturnsAsync(baseHandle); + + Mock.Get(_store) + .Setup(x => x.StoreAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + var code = CreateAuthorizationCode(); + var sut = CreateSut(); + + await sut.StoreAuthorizationCodeAsync(code); + + Mock.Get(_telemetry) + .Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Stores, sut, "StoreAuthorizationCodeAsync")); + Mock.Get(_trace).Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task GetAuthorizationCodeAsync_WhenGrantExists_ShouldTelemetryTrace() + { + Mock.Get(_telemetry).Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace); + + const string handle = "test_handle-1"; + var code = CreateAuthorizationCode(); + + var grant = new PersistedGrant + { + Key = GetHashedKey(handle), + Type = IdentityServerConstants.PersistedGrantTypes.AuthorizationCode, + ClientId = code.ClientId, + Data = _serializer.Serialize(code), + }; + + Mock.Get(_store) + .Setup(x => x.GetAsync(GetHashedKey(handle))) + .ReturnsAsync(grant); + + var sut = CreateSut(); + + await sut.GetAuthorizationCodeAsync(handle); + + Mock.Get(_telemetry) + .Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Stores, sut, "GetAuthorizationCodeAsync")); + Mock.Get(_trace).Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task RemoveAuthorizationCodeAsync_WhenCalled_ShouldTelemetryTrace() + { + Mock.Get(_telemetry).Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace); + const string handle = "test_handle-1"; + + Mock.Get(_store) + .Setup(x => x.RemoveAsync(GetHashedKey(handle))) + .Returns(Task.CompletedTask); + + var sut = CreateSut(); + + await sut.RemoveAuthorizationCodeAsync(handle); + + Mock.Get(_telemetry) + .Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Stores, sut, "RemoveAuthorizationCodeAsync")); + Mock.Get(_trace).Verify(t => t.Dispose(), Times.Once); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Default/DefaultPersistedGrantStoreTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Default/DefaultPersistedGrantStoreTests.cs index c5c72f7f8..6e1081bda 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Default/DefaultPersistedGrantStoreTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Default/DefaultPersistedGrantStoreTests.cs @@ -8,7 +8,7 @@ using System.Security.Claims; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Common; using Microsoft.Extensions.Logging; using Moq; using Open.IdentityServer; @@ -16,9 +16,10 @@ using Open.IdentityServer.Models; using Open.IdentityServer.Stores; using Open.IdentityServer.Stores.Serialization; +using Open.IdentityServer.UnitTests.Validation.Setup; using Xunit; -namespace IdentityServer.UnitTests.Stores.Default; +namespace Open.IdentityServer.UnitTests.Stores.Default; public class DefaultPersistedGrantStoreTests { @@ -38,18 +39,22 @@ public DefaultPersistedGrantStoreTests() _codes = new DefaultAuthorizationCodeStore(_store, new PersistentGrantSerializer(), _stubHandleGenerationService, + new NopTelemetryService(), TestLogger.Create()); _refreshTokens = new DefaultRefreshTokenStore(_store, new PersistentGrantSerializer(), _stubHandleGenerationService, + new NopTelemetryService(), refreshTokenStoreLogger); _referenceTokens = new DefaultReferenceTokenStore(_store, new PersistentGrantSerializer(), _stubHandleGenerationService, + new NopTelemetryService(), TestLogger.Create()); _userConsent = new DefaultUserConsentStore(_store, new PersistentGrantSerializer(), _stubHandleGenerationService, + new NopTelemetryService(), TestLogger.Create()); } diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Default/DefaultReferenceTokenStoreTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Default/DefaultReferenceTokenStoreTests.cs new file mode 100644 index 000000000..880f8cd92 --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Default/DefaultReferenceTokenStoreTests.cs @@ -0,0 +1,263 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using AwesomeAssertions; +using Moq; +using Open.IdentityServer.Models; +using Open.IdentityServer.Services; +using Open.IdentityServer.Stores; +using Open.IdentityServer.Stores.Serialization; +using Open.IdentityServer.UnitTests.Common; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Stores.Default; + +public class DefaultReferenceTokenStoreTests +{ + private readonly IPersistedGrantStore _store = Mock.Of(); + private readonly IPersistentGrantSerializer _serializer = new PersistentGrantSerializer(); + private readonly IHandleGenerationService _handleGenerationService = Mock.Of(); + private readonly ITelemetryService _telemetry = Mock.Of(); + private readonly ITrace _trace = Mock.Of(); + + private DefaultReferenceTokenStore CreateSut() => + new DefaultReferenceTokenStore( + _store, + _serializer, + _handleGenerationService, + _telemetry, + TestLogger.Create() + ); + + private static Token CreateToken() => new() + { + ClientId = "test-client", + Claims = new[] + { + new Claim("sub", "user-123"), + new Claim("sid", "session-abc"), + }, + Description = "test description", + CreationTime = DateTime.UtcNow, + Lifetime = 3600, + }; + + private static string GetHashedKey(string handle) => + $"{handle}:{IdentityServerConstants.PersistedGrantTypes.ReferenceToken}" + .Sha256(handle.EndsWith(DefaultReferenceTokenStore.HexEncodingSuffix)); + + [Fact] + public async Task StoreReferenceTokenAsync_WhenCalled_ShouldPersistGrantAndReturnHandle() + { + const string baseHandle = "test_base_handle"; + var expectedHandle = baseHandle + DefaultReferenceTokenStore.HexEncodingSuffix; + + Mock.Get(_handleGenerationService) + .Setup(x => x.GenerateAsync()) + .ReturnsAsync(baseHandle); + + Mock.Get(_store) + .Setup(x => x.StoreAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + var token = CreateToken(); + var sut = CreateSut(); + + var handle = await sut.StoreReferenceTokenAsync(token); + + handle.Should().Be(expectedHandle); + Mock.Get(_store).Verify(x => x.StoreAsync(It.Is(g => + g.Key == GetHashedKey(expectedHandle) && + g.Type == IdentityServerConstants.PersistedGrantTypes.ReferenceToken && + g.ClientId == token.ClientId && + g.SessionId == token.SessionId + )), Times.Once); + } + + [Fact] + public async Task GetReferenceTokenAsync_WhenGrantExists_ShouldReturnDeserializedToken() + { + const string handle = "test_handle-1"; + var token = CreateToken(); + + var grant = new PersistedGrant + { + Key = GetHashedKey(handle), + Type = IdentityServerConstants.PersistedGrantTypes.ReferenceToken, + ClientId = token.ClientId, + Data = _serializer.Serialize(token), + }; + + Mock.Get(_store) + .Setup(x => x.GetAsync(GetHashedKey(handle))) + .ReturnsAsync(grant); + + var sut = CreateSut(); + + var result = await sut.GetReferenceTokenAsync(handle); + + result.Should().NotBeNull(); + result.ClientId.Should().Be(token.ClientId); + result.SessionId.Should().Be(token.SessionId); + result.Lifetime.Should().Be(token.Lifetime); + } + + [Fact] + public async Task GetReferenceTokenAsync_WhenGrantNotFound_ShouldReturnNull() + { + const string handle = "missing_handle-1"; + + Mock.Get(_store) + .Setup(x => x.GetAsync(GetHashedKey(handle))) + .ReturnsAsync((PersistedGrant)null); + + var sut = CreateSut(); + + var result = await sut.GetReferenceTokenAsync(handle); + + result.Should().BeNull(); + } + + [Fact] + public async Task RemoveReferenceTokenAsync_WhenCalled_ShouldRemoveGrantByHashedKey() + { + const string handle = "test_handle-1"; + + Mock.Get(_store) + .Setup(x => x.RemoveAsync(GetHashedKey(handle))) + .Returns(Task.CompletedTask); + + var sut = CreateSut(); + + await sut.RemoveReferenceTokenAsync(handle); + + Mock.Get(_store).Verify(x => x.RemoveAsync(GetHashedKey(handle)), Times.Once); + } + + [Fact] + public async Task RemoveReferenceTokensAsync_WhenCalled_ShouldRemoveAllGrantsBySubjectAndClient() + { + const string subjectId = "user-123"; + const string clientId = "test-client"; + + Mock.Get(_store) + .Setup(x => x.RemoveAllAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + var sut = CreateSut(); + + await sut.RemoveReferenceTokensAsync(subjectId, clientId); + + Mock.Get(_store).Verify(x => x.RemoveAllAsync(It.Is(f => + f.SubjectId == subjectId && + f.ClientId == clientId && + f.Type == IdentityServerConstants.PersistedGrantTypes.ReferenceToken + )), Times.Once); + } + + [Fact] + public async Task StoreReferenceTokenAsync_WhenCalled_ShouldTelemetryTrace() + { + Mock.Get(_telemetry).Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace); + + const string baseHandle = "test_base_handle"; + + Mock.Get(_handleGenerationService) + .Setup(x => x.GenerateAsync()) + .ReturnsAsync(baseHandle); + + Mock.Get(_store) + .Setup(x => x.StoreAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + var token = CreateToken(); + var sut = CreateSut(); + + await sut.StoreReferenceTokenAsync(token); + + Mock.Get(_telemetry) + .Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Stores, sut,"StoreReferenceTokenAsync")); + Mock.Get(_trace).Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task GetReferenceTokenAsync_WhenCalled_ShouldTelemetryTrace() + { + Mock.Get(_telemetry).Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace); + + const string handle = "test_handle-1"; + var token = CreateToken(); + + var grant = new PersistedGrant + { + Key = GetHashedKey(handle), + Type = IdentityServerConstants.PersistedGrantTypes.ReferenceToken, + ClientId = token.ClientId, + Data = _serializer.Serialize(token), + }; + + Mock.Get(_store) + .Setup(x => x.GetAsync(GetHashedKey(handle))) + .ReturnsAsync(grant); + + var sut = CreateSut(); + + await sut.GetReferenceTokenAsync(handle); + + Mock.Get(_telemetry) + .Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Stores, sut,"GetReferenceTokenAsync")); + Mock.Get(_trace).Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task RemoveReferenceTokenAsync_WhenCalled_ShouldTelemetryTrace() + { + Mock.Get(_telemetry).Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace); + + const string handle = "test_handle-1"; + + Mock.Get(_store) + .Setup(x => x.RemoveAsync(GetHashedKey(handle))) + .Returns(Task.CompletedTask); + + var sut = CreateSut(); + + await sut.RemoveReferenceTokenAsync(handle); + + Mock.Get(_telemetry) + .Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Stores, sut, "RemoveReferenceTokenAsync")); + Mock.Get(_trace).Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task RemoveReferenceTokensAsync_WhenCalled_ShouldTelemetryTrace() + { + Mock.Get(_telemetry).Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace); + + const string subjectId = "user-123"; + const string clientId = "test-client"; + + Mock.Get(_store) + .Setup(x => x.RemoveAllAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + var sut = CreateSut(); + + await sut.RemoveReferenceTokensAsync(subjectId, clientId); + + Mock.Get(_telemetry) + .Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Stores, sut, "RemoveReferenceTokensAsync")); + Mock.Get(_trace).Verify(t => t.Dispose(), Times.Once); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Default/DefaultRefreshTokenStoreTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Default/DefaultRefreshTokenStoreTests.cs new file mode 100644 index 000000000..32e2c6fda --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Default/DefaultRefreshTokenStoreTests.cs @@ -0,0 +1,301 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using AwesomeAssertions; +using Moq; +using Open.IdentityServer.Models; +using Open.IdentityServer.Services; +using Open.IdentityServer.Stores; +using Open.IdentityServer.Stores.Serialization; +using Open.IdentityServer.UnitTests.Common; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Stores.Default; + +public class DefaultRefreshTokenStoreTests +{ + private readonly IPersistedGrantStore _store = Mock.Of(); + private readonly IPersistentGrantSerializer _serializer = new PersistentGrantSerializer(); + private readonly IHandleGenerationService _handleGenerationService = Mock.Of(); + private readonly ITelemetryService _telemetry = Mock.Of(); + private readonly ITrace _trace = Mock.Of(); + + private DefaultRefreshTokenStore CreateSut() => + new DefaultRefreshTokenStore( + _store, + _serializer, + _handleGenerationService, + _telemetry, + TestLogger.Create() + ); + + private static RefreshToken CreateRefreshToken() => new() + { + ClientId = "test-client", + Subject = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim("sub", "user-123") }, "test")), + SessionId = "session-abc", + Description = "test description", + CreationTime = DateTime.UtcNow, + Lifetime = 3600, + }; + + private static string GetHashedKey(string handle) => + $"{handle}:{IdentityServerConstants.PersistedGrantTypes.RefreshToken}" + .Sha256(handle.EndsWith(DefaultRefreshTokenStore.HexEncodingSuffix)); + + [Fact] + public async Task StoreRefreshTokenAsync_WhenCalled_ShouldPersistGrantAndReturnHandle() + { + const string baseHandle = "test_base_handle"; + var expectedHandle = baseHandle + DefaultRefreshTokenStore.HexEncodingSuffix; + + Mock.Get(_handleGenerationService) + .Setup(x => x.GenerateAsync()) + .ReturnsAsync(baseHandle); + + Mock.Get(_store) + .Setup(x => x.StoreAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + var token = CreateRefreshToken(); + var sut = CreateSut(); + + var handle = await sut.StoreRefreshTokenAsync(token); + + handle.Should().Be(expectedHandle); + Mock.Get(_store).Verify(x => x.StoreAsync(It.Is(g => + g.Key == GetHashedKey(expectedHandle) && + g.Type == IdentityServerConstants.PersistedGrantTypes.RefreshToken && + g.ClientId == token.ClientId && + g.SessionId == token.SessionId + )), Times.Once); + } + + [Fact] + public async Task GetRefreshTokenAsync_WhenGrantExists_ShouldReturnDeserializedToken() + { + const string handle = "test_handle-1"; + var token = CreateRefreshToken(); + + var grant = new PersistedGrant + { + Key = GetHashedKey(handle), + Type = IdentityServerConstants.PersistedGrantTypes.RefreshToken, + ClientId = token.ClientId, + Data = _serializer.Serialize(token), + }; + + Mock.Get(_store) + .Setup(x => x.GetAsync(GetHashedKey(handle))) + .ReturnsAsync(grant); + + var sut = CreateSut(); + + var result = await sut.GetRefreshTokenAsync(handle); + + result.Should().NotBeNull(); + result.ClientId.Should().Be(token.ClientId); + result.SessionId.Should().Be(token.SessionId); + result.Lifetime.Should().Be(token.Lifetime); + } + + [Fact] + public async Task GetRefreshTokenAsync_WhenGrantNotFound_ShouldReturnNull() + { + const string handle = "missing_handle-1"; + + Mock.Get(_store) + .Setup(x => x.GetAsync(GetHashedKey(handle))) + .ReturnsAsync((PersistedGrant)null); + + var sut = CreateSut(); + + var result = await sut.GetRefreshTokenAsync(handle); + + result.Should().BeNull(); + } + + [Fact] + public async Task UpdateRefreshTokenAsync_WhenCalled_ShouldStoreGrantWithCorrectProperties() + { + const string handle = "existing_handle-1"; + var consumedTime = DateTime.UtcNow; + var token = CreateRefreshToken(); + token.ConsumedTime = consumedTime; + + Mock.Get(_store) + .Setup(x => x.StoreAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + var sut = CreateSut(); + + await sut.UpdateRefreshTokenAsync(handle, token); + + Mock.Get(_store).Verify(x => x.StoreAsync(It.Is(g => + g.Key == GetHashedKey(handle) && + g.Type == IdentityServerConstants.PersistedGrantTypes.RefreshToken && + g.ClientId == token.ClientId && + g.ConsumedTime == consumedTime + )), Times.Once); + } + + [Fact] + public async Task RemoveRefreshTokenAsync_WhenCalled_ShouldRemoveGrantByHashedKey() + { + const string handle = "test_handle-1"; + + Mock.Get(_store) + .Setup(x => x.RemoveAsync(GetHashedKey(handle))) + .Returns(Task.CompletedTask); + + var sut = CreateSut(); + + await sut.RemoveRefreshTokenAsync(handle); + + Mock.Get(_store).Verify(x => x.RemoveAsync(GetHashedKey(handle)), Times.Once); + } + + [Fact] + public async Task RemoveRefreshTokensAsync_WhenCalled_ShouldRemoveAllGrantsBySubjectAndClient() + { + const string subjectId = "user-123"; + const string clientId = "test-client"; + + Mock.Get(_store) + .Setup(x => x.RemoveAllAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + var sut = CreateSut(); + + await sut.RemoveRefreshTokensAsync(subjectId, clientId); + + Mock.Get(_store).Verify(x => x.RemoveAllAsync(It.Is(f => + f.SubjectId == subjectId && + f.ClientId == clientId && + f.Type == IdentityServerConstants.PersistedGrantTypes.RefreshToken + )), Times.Once); + } + + [Fact] + public async Task StoreRefreshTokenAsync_WhenCalled_ShouldTelemetryTrace() + { + Mock.Get(_telemetry).Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace); + + const string baseHandle = "test_base_handle"; + var expectedHandle = baseHandle + DefaultRefreshTokenStore.HexEncodingSuffix; + + Mock.Get(_handleGenerationService) + .Setup(x => x.GenerateAsync()) + .ReturnsAsync(baseHandle); + + Mock.Get(_store) + .Setup(x => x.StoreAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + var token = CreateRefreshToken(); + var sut = CreateSut(); + + await sut.StoreRefreshTokenAsync(token); + + Mock.Get(_telemetry) + .Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Stores, sut, "StoreRefreshTokenAsync")); + Mock.Get(_trace).Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task UpdateRefreshTokenAsync_WhenCalled_ShouldTelemetryTrace() + { + Mock.Get( _telemetry).Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace); + + const string handle = "existing_handle-1"; + var consumedTime = DateTime.UtcNow; + var token = CreateRefreshToken(); + token.ConsumedTime = consumedTime; + + Mock.Get(_store) + .Setup(x => x.StoreAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + var sut = CreateSut(); + + await sut.UpdateRefreshTokenAsync(handle, token); + + Mock.Get(_telemetry) + .Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Stores, sut, "UpdateRefreshTokenAsync")); + Mock.Get(_trace).Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task GetRefreshTokenAsync_WhenCalled_ShouldTelemetryTrace() + { + Mock.Get(_telemetry).Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace); + + const string handle = "handle-1"; + + Mock.Get(_store) + .Setup(x => x.GetAsync(GetHashedKey(handle))) + .ReturnsAsync((PersistedGrant)null); + + var sut = CreateSut(); + + await sut.GetRefreshTokenAsync(handle); + + Mock.Get(_telemetry) + .Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Stores, sut, "GetRefreshTokenAsync")); + Mock.Get(_trace).Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task RemoveRefreshTokenAsync_WhenCalled_ShouldTelemetryTrace() + { + Mock.Get(_telemetry).Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace); + + const string handle = "test_handle-1"; + + Mock.Get(_store) + .Setup(x => x.RemoveAsync(GetHashedKey(handle))) + .Returns(Task.CompletedTask); + + var sut = CreateSut(); + + await sut.RemoveRefreshTokenAsync(handle); + + Mock.Get(_telemetry) + .Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Stores, sut, "RemoveRefreshTokenAsync")); + Mock.Get(_trace).Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task RemoveRefreshTokensAsync_WhenCalled_ShouldTelemetryTrace() + { + Mock.Get(_telemetry).Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace); + + const string subjectId = "user-123"; + const string clientId = "test-client"; + + Mock.Get(_store) + .Setup(x => x.RemoveAllAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + var sut = CreateSut(); + + await sut.RemoveRefreshTokensAsync(subjectId, clientId); + + Mock.Get(_telemetry) + .Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Stores, sut, "RemoveRefreshTokensAsync")); + Mock.Get(_trace).Verify(t => t.Dispose(), Times.Once); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Default/DefaultUserConsentStoreTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Default/DefaultUserConsentStoreTests.cs index 71c912659..034fcb8bb 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Default/DefaultUserConsentStoreTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/Default/DefaultUserConsentStoreTests.cs @@ -15,18 +15,20 @@ using Open.IdentityServer.Stores.Serialization; using Xunit; -namespace IdentityServer.UnitTests.Stores.Default; +namespace Open.IdentityServer.UnitTests.Stores.Default; public class DefaultUserConsentStoreTests { - private readonly IPersistedGrantStore store = Mock.Of(); - private readonly IPersistentGrantSerializer serializer = new PersistentGrantSerializer(); - private readonly IHandleGenerationService handleGenerationService = Mock.Of(); - private readonly ILogger logger = NullLogger.Instance; + private readonly IPersistedGrantStore _store = Mock.Of(); + private readonly IPersistentGrantSerializer _serializer = new PersistentGrantSerializer(); + private readonly IHandleGenerationService _handleGenerationService = Mock.Of(); + private readonly ITelemetryService _telemetry = Mock.Of(); + private readonly ITrace _trace = Mock.Of(); + private readonly ILogger _logger = NullLogger.Instance; private DefaultUserConsentStore CreateSut() { - return new DefaultUserConsentStore(store, serializer, handleGenerationService, logger); + return new DefaultUserConsentStore(_store, _serializer, _handleGenerationService, _telemetry, _logger); } [Fact] @@ -46,7 +48,7 @@ public async Task GetUserConsentAsync_WhenHexEncodedKeyReturnsValue_ShouldReturn Type = IdentityServerConstants.PersistedGrantTypes.UserConsent, }; - Mock.Get(store) + Mock.Get(_store) .Setup(x => x.GetAsync(GetHexHashedKey(fakeConsent.ClientId, fakeConsent.SubjectId))) .ReturnsAsync(grant); @@ -74,11 +76,11 @@ public async Task GetUserConsentAsync_WhenHexEncodedKeyReturnsNull_ShouldTryAgai Type = IdentityServerConstants.PersistedGrantTypes.UserConsent, }; - Mock.Get(store) + Mock.Get(_store) .Setup(x => x.GetAsync(GetHexHashedKey(fakeConsent.ClientId, fakeConsent.SubjectId))) .ReturnsAsync((PersistedGrant) null); - Mock.Get(store) + Mock.Get(_store) .Setup(x => x.GetAsync(GetBase64HashedKey(fakeConsent.ClientId, fakeConsent.SubjectId))) .ReturnsAsync(grant); @@ -98,11 +100,11 @@ public async Task GetUserConsentAsync_WhenHexEncodedKeyAndBase64ReturnsNull_Shou ClientId = "fake.client", }; - Mock.Get(store) + Mock.Get(_store) .Setup(x => x.GetAsync(GetHexHashedKey(fakeConsent.ClientId, fakeConsent.SubjectId))) .ReturnsAsync((PersistedGrant) null); - Mock.Get(store) + Mock.Get(_store) .Setup(x => x.GetAsync(GetBase64HashedKey(fakeConsent.ClientId, fakeConsent.SubjectId))) .ReturnsAsync((PersistedGrant) null); @@ -120,4 +122,100 @@ private string GetHexHashedKey(string clientId, string subjectId) => private string GetBase64HashedKey(string clientId, string subjectId) => $"{clientId}|{subjectId}:{IdentityServerConstants.PersistedGrantTypes.UserConsent}" .Sha256(); + + [Fact] + public async Task StoreUserConsentAsync_WhenCalled_ShouldTelemetryTrace() + { + Mock.Get(_telemetry).Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace); + + const string baseHandle = "test_base_handle"; + var expectedHandle = baseHandle + DefaultAuthorizationCodeStore.HexEncodingSuffix; + + Mock.Get(_handleGenerationService) + .Setup(x => x.GenerateAsync()) + .ReturnsAsync(baseHandle); + + Mock.Get(_store) + .Setup(x => x.StoreAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + var consent = new Consent(); + var sut = CreateSut(); + + await sut.StoreUserConsentAsync(consent); + + Mock.Get(_telemetry) + .Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Stores, sut, "StoreUserConsentAsync")); + Mock.Get(_trace).Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task GetUserConsentAsync_WhenCalled_ShouldTelemetryTrace() + { + Mock.Get(_telemetry).Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace); + + var fakeConsent = new Consent + { + SubjectId = Guid.NewGuid().ToString(), + ClientId = "fake.client", + }; + + var grant = new PersistedGrant + { + SubjectId = fakeConsent.SubjectId, + ClientId = fakeConsent.ClientId, + Data = JsonSerializer.Serialize(fakeConsent), + Type = IdentityServerConstants.PersistedGrantTypes.UserConsent, + }; + + Mock.Get(_store) + .Setup(x => x.GetAsync(GetHexHashedKey(fakeConsent.ClientId, fakeConsent.SubjectId))) + .ReturnsAsync(grant); + + var sut = CreateSut(); + + await sut.GetUserConsentAsync(fakeConsent.SubjectId, fakeConsent.ClientId); + + Mock.Get(_telemetry) + .Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Stores, sut, "GetUserConsentAsync")); + Mock.Get(_trace).Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task RemoveUserConsentAsync_WhenCalled_ShouldTelemetryTrace() + { + Mock.Get(_telemetry).Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace); + + var fakeConsent = new Consent + { + SubjectId = Guid.NewGuid().ToString(), + ClientId = "fake.client", + }; + + var grant = new PersistedGrant + { + SubjectId = fakeConsent.SubjectId, + ClientId = fakeConsent.ClientId, + Data = JsonSerializer.Serialize(fakeConsent), + Type = IdentityServerConstants.PersistedGrantTypes.UserConsent, + }; + + Mock.Get(_store) + .Setup(x => x.GetAsync(GetHexHashedKey(fakeConsent.ClientId, fakeConsent.SubjectId))) + .ReturnsAsync(grant); + + var sut = CreateSut(); + + await sut.RemoveUserConsentAsync(fakeConsent.SubjectId, fakeConsent.ClientId); + + Mock.Get(_telemetry) + .Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Stores, sut, "RemoveUserConsentAsync")); + Mock.Get(_trace).Verify(t => t.Dispose(), Times.Once); + } } \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/InMemoryClientStoreTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/InMemoryClientStoreTests.cs index 5007244ca..0123e8452 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/InMemoryClientStoreTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/InMemoryClientStoreTests.cs @@ -8,7 +8,7 @@ using Xunit; using AwesomeAssertions; -namespace IdentityServer.UnitTests.Stores; +namespace Open.IdentityServer.UnitTests.Stores; public class InMemoryClientStoreTests { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/InMemoryDeviceFlowStoreTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/InMemoryDeviceFlowStoreTests.cs index be7fc750e..b38221608 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/InMemoryDeviceFlowStoreTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/InMemoryDeviceFlowStoreTests.cs @@ -10,7 +10,7 @@ using Open.IdentityServer.Stores; using Xunit; -namespace IdentityServer.UnitTests.Stores; +namespace Open.IdentityServer.UnitTests.Stores; public class InMemoryDeviceFlowStoreTests { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/InMemoryPersistedGrantStoreTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/InMemoryPersistedGrantStoreTests.cs index 19c428636..9ffb4c2ff 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/InMemoryPersistedGrantStoreTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/InMemoryPersistedGrantStoreTests.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; using System.Linq; -namespace IdentityServer.UnitTests.Stores; +namespace Open.IdentityServer.UnitTests.Stores; public class InMemoryPersistedGrantStoreTests { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/InMemoryResourcesStoreTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/InMemoryResourcesStoreTests.cs index a4ca259b1..cf6cb5293 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/InMemoryResourcesStoreTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/InMemoryResourcesStoreTests.cs @@ -8,7 +8,7 @@ using Xunit; using AwesomeAssertions; -namespace IdentityServer.UnitTests.Stores; +namespace Open.IdentityServer.UnitTests.Stores; public class InMemoryResourcesStoreTests { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/ValidatingClientStoreTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/ValidatingClientStoreTests.cs new file mode 100644 index 000000000..673571ea9 --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Stores/ValidatingClientStoreTests.cs @@ -0,0 +1,184 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Threading.Tasks; +using AwesomeAssertions; +using Open.IdentityServer.UnitTests.Common; +using Moq; +using Open.IdentityServer; +using Open.IdentityServer.Events; +using Open.IdentityServer.Models; +using Open.IdentityServer.Services; +using Open.IdentityServer.Stores; +using Open.IdentityServer.Validation; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Stores; + +public class ValidatingClientStoreTests +{ + private const string Category = "Stores - ValidatingClientStore"; + + private readonly Mock _inner = new(); + private readonly Mock _validator = new(); + private readonly TestEventService _events = new(); + private readonly Mock _telemetry = new(); + private readonly Mock _trace = new(); + + private ValidatingClientStore CreateSubject() + { + return new ValidatingClientStore( + _inner.Object, + _validator.Object, + _events, + _telemetry.Object, + TestLogger.Create>()); + } + + [Fact] + [Trait("Category", Category)] + public async Task find_client_should_return_null_when_inner_store_returns_null() + { + var subject = CreateSubject(); + + _inner.Setup(x => x.FindClientByIdAsync("client")) + .ReturnsAsync((Client)null); + + var result = await subject.FindClientByIdAsync("client"); + + result.Should().BeNull(); + _validator.Verify(x => x.ValidateAsync(It.IsAny()), Times.Never); + } + + [Fact] + [Trait("Category", Category)] + public async Task find_client_should_validate_the_client_loaded_from_inner_store() + { + var subject = CreateSubject(); + var client = new Client { ClientId = "expected-client" }; + ClientConfigurationValidationContext captured = null; + + _inner.Setup(x => x.FindClientByIdAsync("expected-client")) + .ReturnsAsync(client); + + _validator.Setup(x => x.ValidateAsync(It.IsAny())) + .Callback(ctx => captured = ctx) + .Returns(Task.CompletedTask); + + await subject.FindClientByIdAsync("expected-client"); + + captured.Should().NotBeNull(); + captured.Client.Should().BeSameAs(client); + } + + [Fact] + [Trait("Category", Category)] + public async Task find_client_should_return_client_when_configuration_is_valid() + { + var subject = CreateSubject(); + var client = new Client { ClientId = "client" }; + + _inner.Setup(x => x.FindClientByIdAsync("client")) + .ReturnsAsync(client); + + _validator.Setup(x => x.ValidateAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + var result = await subject.FindClientByIdAsync("client"); + + result.Should().NotBeNull(); + result.Should().BeSameAs(client); + _validator.Verify(x => x.ValidateAsync(It.IsAny()), Times.Once); + } + + [Fact] + [Trait("Category", Category)] + public async Task find_client_should_return_null_and_raise_event_when_configuration_is_invalid() + { + var subject = CreateSubject(); + var client = new Client { ClientId = "client" }; + + _inner.Setup(x => x.FindClientByIdAsync("client")) + .ReturnsAsync(client); + + _validator.Setup(x => x.ValidateAsync(It.IsAny())) + .Callback(ctx => + { + ctx.SetError("invalid client configuration"); + }) + .Returns(Task.CompletedTask); + + var result = await subject.FindClientByIdAsync("client"); + + result.Should().BeNull(); + _validator.Verify(x => x.ValidateAsync(It.IsAny()), Times.Once); + _events.AssertEventWasRaised(); + } + + [Fact] + public async Task FindClientById_OnValidClient_ShouldRaiseTelemetrySignal() + { + _telemetry.Setup(t => t.CountClientConfigValidation( "client", null)) + .Verifiable(Times.Once()); + + var subject = CreateSubject(); + var client = new Client { ClientId = "client" }; + + _inner.Setup(x => x.FindClientByIdAsync("client")) + .ReturnsAsync(client); + + _validator.Setup(x => x.ValidateAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + var result = await subject.FindClientByIdAsync("client"); + + _telemetry.Verify(); + } + + [Fact] + public async Task FindClientById_OnInvalidClient_ShouldRaiseTelemetrySignal() + { + _telemetry.Setup(t => t.CountClientConfigValidation("client", "invalid client configuration")) + .Verifiable(Times.Once()); + + var subject = CreateSubject(); + var client = new Client { ClientId = "client" }; + + _inner.Setup(x => x.FindClientByIdAsync("client")) + .ReturnsAsync(client); + + _validator.Setup(x => x.ValidateAsync(It.IsAny())) + .Callback(ctx => + { + ctx.SetError("invalid client configuration"); + }) + .Returns(Task.CompletedTask); + + var result = await subject.FindClientByIdAsync("client"); + + _telemetry.Verify(); + } + + [Fact] + public async Task FindClientById_WhenCalled_ShouldTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var subject = CreateSubject(); + var client = new Client { ClientId = "client" }; + + _inner.Setup(x => x.FindClientByIdAsync("client")) + .ReturnsAsync(client); + + _validator.Setup(x => x.ValidateAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + await subject.FindClientByIdAsync("client"); + + _telemetry.Verify( + t => t.Trace( + TelemetryConstants.TraceCategories.Stores, subject, "FindClientByIdAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AccessTokenValidation.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AccessTokenValidation.cs index a69d5f87e..b33c8b9b9 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AccessTokenValidation.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AccessTokenValidation.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -7,15 +8,17 @@ using System.Linq; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; -using IdentityServer.UnitTests.Validation.Setup; +using Moq; +using Open.IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Validation.Setup; using Open.IdentityServer; using Open.IdentityServer.Configuration; using Open.IdentityServer.Models; +using Open.IdentityServer.Services; using Open.IdentityServer.Stores; using Xunit; -namespace IdentityServer.UnitTests.Validation; +namespace Open.IdentityServer.UnitTests.Validation; public class AccessTokenValidation { @@ -240,4 +243,24 @@ public async Task Valid_AccessToken_but_Client_not_active() result.IsError.Should().BeTrue(); } + + [Fact] + public async Task AccessToken_Should_TraceTelemetry() + { + var telemetry = new Mock(); + var trace = new Mock(); + + telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(trace.Object); + + var signer = Factory.CreateDefaultTokenCreator(); + var jwt = await signer.CreateTokenAsync(TokenFactory.CreateAccessToken(new Client { ClientId = "roclient" }, "valid", 600, "read", "write")); + + var validator = Factory.CreateTokenValidator(telemetry: telemetry.Object); + await validator.ValidateAccessTokenAsync(jwt); + + telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Validation, validator, "ValidateAccessTokenAsync")); + trace.Verify(t => t.Dispose(), Times.Once); + } } \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Agnostic.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Agnostic.cs new file mode 100644 index 000000000..07543803f --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Agnostic.cs @@ -0,0 +1,33 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Collections.Specialized; +using System.Threading.Tasks; +using Moq; +using Open.IdentityServer.Services; +using Open.IdentityServer.UnitTests.Validation.Setup; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Validation.AuthorizeRequest_Validation; + +public class Agnostic +{ + [Fact] + public async Task ValidateAsync_WhenCalled_ShouldCreateTelemetryTrace() + { + Mock telemetry = new(); + Mock trace = new(); + + telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(trace.Object); + + var parameters = new NameValueCollection(); + + var validator = Factory.CreateAuthorizeRequestValidator(telemetry: telemetry.Object); + await validator.ValidateAsync(parameters); + + telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Validation, validator, "ValidateAsync")); + trace.Verify(t => t.Dispose(), Times.Once); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ClientValidation_Code.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ClientValidation_Code.cs index caf0fd201..c0fb6f478 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ClientValidation_Code.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ClientValidation_Code.cs @@ -5,13 +5,13 @@ using System.Collections.Specialized; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; -using IdentityServer.UnitTests.Validation.Setup; +using Open.IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Validation.Setup; using Open.IdentityServer; using Open.IdentityServer.Configuration; using Xunit; -namespace IdentityServer.UnitTests.Validation.AuthorizeRequest_Validation; +namespace Open.IdentityServer.UnitTests.Validation.AuthorizeRequest_Validation; public class Authorize_ClientValidation_Code { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ClientValidation_IdToken.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ClientValidation_IdToken.cs index e0f575972..3d2c03452 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ClientValidation_IdToken.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ClientValidation_IdToken.cs @@ -5,13 +5,13 @@ using System.Collections.Specialized; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; -using IdentityServer.UnitTests.Validation.Setup; +using Open.IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Validation.Setup; using Open.IdentityServer; using Open.IdentityServer.Configuration; using Xunit; -namespace IdentityServer.UnitTests.Validation.AuthorizeRequest_Validation; +namespace Open.IdentityServer.UnitTests.Validation.AuthorizeRequest_Validation; public class Authorize_ClientValidation_IdToken { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ClientValidation_Invalid.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ClientValidation_Invalid.cs index e7dccda33..6b367204a 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ClientValidation_Invalid.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ClientValidation_Invalid.cs @@ -5,13 +5,13 @@ using System.Collections.Specialized; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; -using IdentityServer.UnitTests.Validation.Setup; +using Open.IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Validation.Setup; using Open.IdentityServer; using Open.IdentityServer.Configuration; using Xunit; -namespace IdentityServer.UnitTests.Validation.AuthorizeRequest_Validation; +namespace Open.IdentityServer.UnitTests.Validation.AuthorizeRequest_Validation; public class Authorize_ClientValidation_Invalid { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ClientValidation_Token.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ClientValidation_Token.cs index 2069ce752..026d87fcf 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ClientValidation_Token.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ClientValidation_Token.cs @@ -5,13 +5,13 @@ using System.Collections.Specialized; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; -using IdentityServer.UnitTests.Validation.Setup; +using Open.IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Validation.Setup; using Open.IdentityServer; using Open.IdentityServer.Configuration; using Xunit; -namespace IdentityServer.UnitTests.Validation.AuthorizeRequest_Validation; +namespace Open.IdentityServer.UnitTests.Validation.AuthorizeRequest_Validation; public class Authorize_ClientValidation_Token { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ClientValidation_Valid.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ClientValidation_Valid.cs index d27282d2b..146c95d34 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ClientValidation_Valid.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ClientValidation_Valid.cs @@ -5,13 +5,13 @@ using System.Collections.Specialized; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; -using IdentityServer.UnitTests.Validation.Setup; +using Open.IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Validation.Setup; using Open.IdentityServer; using Open.IdentityServer.Configuration; using Xunit; -namespace IdentityServer.UnitTests.Validation.AuthorizeRequest_Validation; +namespace Open.IdentityServer.UnitTests.Validation.AuthorizeRequest_Validation; public class Authorize_ClientValidation_Valid { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ProtocolValidation_CustomValidator.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ProtocolValidation_CustomValidator.cs index 55869a3a7..86ec92283 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ProtocolValidation_CustomValidator.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ProtocolValidation_CustomValidator.cs @@ -6,12 +6,12 @@ using System.Collections.Specialized; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Validation.Setup; +using Open.IdentityServer.UnitTests.Validation.Setup; using Open.IdentityServer; using Open.IdentityServer.Validation; using Xunit; -namespace IdentityServer.UnitTests.Validation.AuthorizeRequest_Validation; +namespace Open.IdentityServer.UnitTests.Validation.AuthorizeRequest_Validation; public class Authorize_ProtocolValidation_CustomValidator { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ProtocolValidation_Invalid.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ProtocolValidation_Invalid.cs index 74bad5426..9efe0f801 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ProtocolValidation_Invalid.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ProtocolValidation_Invalid.cs @@ -6,11 +6,11 @@ using System.Collections.Specialized; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Validation.Setup; +using Open.IdentityServer.UnitTests.Validation.Setup; using Open.IdentityServer; using Xunit; -namespace IdentityServer.UnitTests.Validation.AuthorizeRequest_Validation; +namespace Open.IdentityServer.UnitTests.Validation.AuthorizeRequest_Validation; public class Authorize_ProtocolValidation_Invalid { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ProtocolValidation_PKCE.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ProtocolValidation_PKCE.cs index a5117da24..0efc1a5ef 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ProtocolValidation_PKCE.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ProtocolValidation_PKCE.cs @@ -5,13 +5,13 @@ using System.Collections.Specialized; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; -using IdentityServer.UnitTests.Validation.Setup; +using Open.IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Validation.Setup; using Open.IdentityServer; using Open.IdentityServer.Configuration; using Xunit; -namespace IdentityServer.UnitTests.Validation.AuthorizeRequest_Validation; +namespace Open.IdentityServer.UnitTests.Validation.AuthorizeRequest_Validation; public class Authorize_ProtocolValidation_Valid_PKCE { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ProtocolValidation_Valid.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ProtocolValidation_Valid.cs index 2f17a1c3e..373d100d6 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ProtocolValidation_Valid.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ProtocolValidation_Valid.cs @@ -6,11 +6,11 @@ using System.Linq; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Validation.Setup; +using Open.IdentityServer.UnitTests.Validation.Setup; using Open.IdentityServer; using Xunit; -namespace IdentityServer.UnitTests.Validation.AuthorizeRequest_Validation; +namespace Open.IdentityServer.UnitTests.Validation.AuthorizeRequest_Validation; public class Authorize_ProtocolValidation_Valid { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ResourceIndicators_InValid.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ResourceIndicators_InValid.cs index a7b85ae56..bdd6c23cf 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ResourceIndicators_InValid.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ResourceIndicators_InValid.cs @@ -4,11 +4,11 @@ using System.Collections.Specialized; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Validation.Setup; +using Open.IdentityServer.UnitTests.Validation.Setup; using Open.IdentityServer; using Xunit; -namespace IdentityServer.UnitTests.Validation.AuthorizeRequest_Validation; +namespace Open.IdentityServer.UnitTests.Validation.AuthorizeRequest_Validation; public class Authorize_ResourceIndicators_InValid { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ResourceIndicators_Valid.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ResourceIndicators_Valid.cs index 62fac9fce..9edebc638 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ResourceIndicators_Valid.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/AuthorizeRequest Validation/Authorize_ResourceIndicators_Valid.cs @@ -4,11 +4,11 @@ using System.Collections.Specialized; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Validation.Setup; +using Open.IdentityServer.UnitTests.Validation.Setup; using Open.IdentityServer; using Xunit; -namespace IdentityServer.UnitTests.Validation.AuthorizeRequest_Validation; +namespace Open.IdentityServer.UnitTests.Validation.AuthorizeRequest_Validation; public class Authorize_ResourceIndicators_Valid { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/BearerTokenUsageValidation.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/BearerTokenUsageValidation.cs index b947d6583..7813634ba 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/BearerTokenUsageValidation.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/BearerTokenUsageValidation.cs @@ -7,12 +7,12 @@ using System.Text; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer.Validation; using Microsoft.AspNetCore.Http; using Xunit; -namespace IdentityServer.UnitTests.Validation; +namespace Open.IdentityServer.UnitTests.Validation; [SuppressMessage( "Usage", diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/ClientConfigurationValidation.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/ClientConfigurationValidation.cs index 13dd95b49..1762d2319 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/ClientConfigurationValidation.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/ClientConfigurationValidation.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -8,21 +9,27 @@ using Open.IdentityServer.Validation; using System; using System.Threading.Tasks; -using IdentityServer.UnitTests.Validation.Setup; +using Moq; +using Open.IdentityServer.Services; +using Open.IdentityServer.UnitTests.Validation.Setup; using Xunit; -namespace IdentityServer.UnitTests.Validation; +namespace Open.IdentityServer.UnitTests.Validation; public class ClientConfigurationValidation { private const string Category = "Client Configuration Validation Tests"; private IClientConfigurationValidator _validator; + private Mock _telemetry; + private Mock _trace; IdentityServerOptions _options; public ClientConfigurationValidation() { + _telemetry = new(); + _trace = new(); _options = new IdentityServerOptions(); - _validator = new DefaultClientConfigurationValidator(_options); + _validator = new DefaultClientConfigurationValidator(_options, _telemetry.Object); } [Fact] @@ -480,6 +487,26 @@ public async Task ValidateAllowedCorsOriginsAsync_should_allow_valid_formats(str result.IsValid.Should().BeTrue(); } + [Fact] + [Trait("Category", Category)] + public async Task ValidateAsync_WhenCalled_ShouldTraceTelemetry() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var client = new Client + { + ClientId = "id", + AllowedGrantTypes = GrantTypes.Implicit, + RedirectUris = { "http://client" }, + }; + + await ValidateAsync(client); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Validation, _validator, "ValidateAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } private async Task ValidateAsync(Client client) { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Default/ClientSecretValidatorTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Default/ClientSecretValidatorTests.cs new file mode 100644 index 000000000..24ad8ba07 --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Default/ClientSecretValidatorTests.cs @@ -0,0 +1,246 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Threading.Tasks; +using AwesomeAssertions; +using Open.IdentityServer.UnitTests.Common; +using Microsoft.AspNetCore.Http; +using Moq; +using Open.IdentityServer.Events; +using Open.IdentityServer.Models; +using Open.IdentityServer.Services; +using Open.IdentityServer.Stores; +using Open.IdentityServer.Validation; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Validation.Default; + +public class ClientSecretValidatorTests +{ + private const string Category = "Validation - ClientSecretValidator"; + + private readonly Mock _clients = new(); + private readonly Mock _parser = new(); + private readonly Mock _validator = new(); + private readonly TestEventService _events = new(); + private readonly Mock _telemetry = new(); + private readonly Mock _trace = new(); + + private ClientSecretValidator CreateSubject() + { + return new ClientSecretValidator( + _clients.Object, + _parser.Object, + _validator.Object, + _events, + _telemetry?.Object, + TestLogger.Create()); + } + + [Fact] + [Trait("Category", Category)] + public async Task Validate_WhenNoSecretFound_ShouldFailAndRaiseEvent() + { + var subject = CreateSubject(); + + _parser.Setup(x => x.ParseAsync(It.IsAny())) + .ReturnsAsync((ParsedSecret)null); + + var result = await subject.ValidateAsync(new DefaultHttpContext()); + + result.IsError.Should().BeTrue(); + + _events.AssertEventWasRaised(); + } + + [Fact] + [Trait("Category", Category)] + public async Task Validate_WhenNoSecretFound_ShouldRaiseTelemetrySignal() + { + _telemetry.Setup(t => t.CountClientSecretValidation("unknown", null, "No client id found")) + .Verifiable(Times.Once); + + var subject = CreateSubject(); + + _parser.Setup(x => x.ParseAsync(It.IsAny())) + .ReturnsAsync((ParsedSecret)null); + + var result = await subject.ValidateAsync(new DefaultHttpContext()); + + result.IsError.Should().BeTrue(); + + _events.AssertEventWasRaised(); + + _telemetry.Verify(); + } + + [Fact] + [Trait("Category", Category)] + public async Task Validate_WhenClientUnknown_ShouldFailAndRaiseEvent() + { + var subject = CreateSubject(); + + _parser.Setup(x => x.ParseAsync(It.IsAny())) + .ReturnsAsync(new ParsedSecret { Id = "unknown-client", Type = "SharedSecret" }); + + _clients.Setup(x => x.FindClientByIdAsync("unknown-client")) + .ReturnsAsync((Client)null); + + var result = await subject.ValidateAsync(new DefaultHttpContext()); + + result.IsError.Should().BeTrue(); + _events.AssertEventWasRaised(); + } + + [Fact] + [Trait("Category", Category)] + public async Task Validate_WhenClientUnknown_ShouldRaiseTelemetrySignal() + { + _telemetry.Setup(t => t.CountClientSecretValidation("unknown-client", null, "Unknown client")) + .Verifiable(Times.Once); + + var subject = CreateSubject(); + + _parser.Setup(x => x.ParseAsync(It.IsAny())) + .ReturnsAsync(new ParsedSecret { Id = "unknown-client", Type = "SharedSecret" }); + + _clients.Setup(x => x.FindClientByIdAsync("unknown-client")) + .ReturnsAsync((Client)null); + + var result = await subject.ValidateAsync(new DefaultHttpContext()); + + result.IsError.Should().BeTrue(); + _events.AssertEventWasRaised(); + + _telemetry.Verify(); + } + + [Fact] + [Trait("Category", Category)] + public async Task Validate_WhenClientSecretInvalid_ShouldFailAndRaiseEvent() + { + var subject = CreateSubject(); + var client = new Client { ClientId = "client" }; + + _parser.Setup(x => x.ParseAsync(It.IsAny())) + .ReturnsAsync(new ParsedSecret { Id = "client", Type = "SharedSecret" }); + + _clients.Setup(x => x.FindClientByIdAsync("client")) + .ReturnsAsync(client); + + _validator.Setup(x => x.ValidateAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(new SecretValidationResult { Success = false }); + + var result = await subject.ValidateAsync(new DefaultHttpContext()); + + result.IsError.Should().BeTrue(); + _events.AssertEventWasRaised(); + } + + [Fact] + [Trait("Category", Category)] + public async Task Validate_WhenClientSecretInvalid_ShouldRaiseTelemetrySignal() + { + _telemetry.Setup(t => t.CountClientSecretValidation("client_id", null, "Invalid client secret")) + .Verifiable(Times.Once); + + var subject = CreateSubject(); + var client = new Client { ClientId = "client_id" }; + + _parser.Setup(x => x.ParseAsync(It.IsAny())) + .ReturnsAsync(new ParsedSecret { Id = "client_id", Type = "SharedSecret" }); + + _clients.Setup(x => x.FindClientByIdAsync("client_id")) + .ReturnsAsync(client); + + _validator.Setup(x => x.ValidateAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(new SecretValidationResult { Success = false }); + + var result = await subject.ValidateAsync(new DefaultHttpContext()); + + result.IsError.Should().BeTrue(); + _events.AssertEventWasRaised(); + + _telemetry.Verify(); + } + + [Fact] + [Trait("Category", Category)] + public async Task Validate_WhenClientSecretValid_ShouldPassAndRaiseEvent() + { + var subject = CreateSubject(); + var client = new Client { ClientId = "client", Enabled = true }; + + _parser.Setup(x => x.ParseAsync(It.IsAny())) + .ReturnsAsync(new ParsedSecret { Id = "client", Type = "SharedSecret" }); + + _clients.Setup(x => x.FindClientByIdAsync("client")) + .ReturnsAsync(client); + + _validator.Setup(x => x.ValidateAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(new SecretValidationResult { Success = true }); + + var result = await subject.ValidateAsync(new DefaultHttpContext()); + + result.IsError.Should().BeFalse(); + + result.Client.Should().BeSameAs(client); + + _events.AssertEventWasRaised(); + } + + [Fact] + [Trait("Category", Category)] + public async Task Validate_WhenClientSecretValid_ShouldRaiseTelemetrySignal() + { + _telemetry.Setup(t => t.CountClientSecretValidation("client", "SharedSecret", null)) + .Verifiable(Times.Once); + + var subject = CreateSubject(); + var client = new Client { ClientId = "client", Enabled = true }; + + _parser.Setup(x => x.ParseAsync(It.IsAny())) + .ReturnsAsync(new ParsedSecret { Id = "client", Type = "SharedSecret" }); + + _clients.Setup(x => x.FindClientByIdAsync("client")) + .ReturnsAsync(client); + + _validator.Setup(x => x.ValidateAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(new SecretValidationResult { Success = true }); + + var result = await subject.ValidateAsync(new DefaultHttpContext()); + + result.IsError.Should().BeFalse(); + + result.Client.Should().BeSameAs(client); + + _events.AssertEventWasRaised(); + + _telemetry.Verify(); + } + + [Fact] + public async Task ValidateAsync_WhenCalled_ShouldInitiateTelemetryTrace() + { + var subject = CreateSubject(); + var client = new Client { ClientId = "client", Enabled = true }; + + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + _parser.Setup(x => x.ParseAsync(It.IsAny())) + .ReturnsAsync(new ParsedSecret { Id = "client", Type = "SharedSecret" }); + + _clients.Setup(x => x.FindClientByIdAsync("client")) + .ReturnsAsync(client); + + _validator.Setup(x => x.ValidateAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(new SecretValidationResult { Success = true }); + + await subject.ValidateAsync(new DefaultHttpContext()); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Validation, subject, "ValidateAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Default/DefaultResourceValidatorTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Default/DefaultResourceValidatorTests.cs new file mode 100644 index 000000000..3b56dd38d --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Default/DefaultResourceValidatorTests.cs @@ -0,0 +1,84 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Moq; +using Open.IdentityServer.Models; +using Open.IdentityServer.Services; +using Open.IdentityServer.Stores; +using Open.IdentityServer.UnitTests.Common; +using Open.IdentityServer.Validation; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Validation.Default; + +public class DefaultResourceValidatorTests +{ + private readonly Mock _resourceStore = new(); + private readonly Mock _scopeParser = new(); + private readonly Mock _telemetry = new(); + private readonly Mock _trace = new(); + + private DefaultResourceValidator CreateSubject() + { + return new DefaultResourceValidator( + _resourceStore.Object, + _scopeParser.Object, + _telemetry.Object, + TestLogger.Create()); + } + + [Fact] + public async Task ValidateRequestedResourcesAsync_WhenCalled_ShouldTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var request = new ResourceValidationRequest + { + Scopes = [] + }; + + _scopeParser + .Setup(x => x.ParseScopeValues(It.IsAny>())) + .Returns(new ParsedScopesResult + { + ParsedScopes = [], + Errors = [] + }); + + _resourceStore + .Setup(x => x.FindIdentityResourcesByScopeNameAsync(It.IsAny>())) + .ReturnsAsync([ + new IdentityResource() + { + Name = "test" + } + ]); + _resourceStore + .Setup(x => x.FindApiResourcesByScopeNameAsync(It.IsAny>())) + .ReturnsAsync([ + new ApiResource() + { + Name = "test2" + } + ]); + _resourceStore + .Setup(x => x.FindApiScopesByNameAsync(It.IsAny>())) + .ReturnsAsync([ + new ApiScope() + { + Name = "test3" + } + ]); + + var subject = CreateSubject(); + + await subject.ValidateRequestedResourcesAsync(request); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Validation, subject, "ValidateRequestedResourcesAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Default/JwtRequestValidatorTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Default/JwtRequestValidatorTests.cs new file mode 100644 index 000000000..aeda44781 --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Default/JwtRequestValidatorTests.cs @@ -0,0 +1,49 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Moq; +using Open.IdentityServer.Configuration; +using Open.IdentityServer.Models; +using Open.IdentityServer.Services; +using Open.IdentityServer.UnitTests.Common; +using Open.IdentityServer.Validation; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Validation.Default; + +public class JwtRequestValidatorTests +{ + private Mock _contextAccessor = new(); + private IdentityServerOptions _options = new IdentityServerOptions(); + private Mock _telemetry = new(); + private Mock _trace = new(); + + private JwtRequestValidator CreateSubject() + { + return new JwtRequestValidator( + _contextAccessor.Object, + _options, + _telemetry.Object, + TestLogger.Create()); + } + + [Fact] + public async Task ValidateAsync_WhenCalled_ShouldCreateTelemetryTrace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + Client client = new Client(); + string jwtString = "asdg"; + + var subject = CreateSubject(); + + await subject.ValidateAsync(client, jwtString); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Validation, subject, "ValidateAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Default/TokenRequestValidatorTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Default/TokenRequestValidatorTests.cs new file mode 100644 index 000000000..1b143be04 --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Default/TokenRequestValidatorTests.cs @@ -0,0 +1,328 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Collections.Specialized; +using System.Threading.Tasks; +using AwesomeAssertions; +using Moq; +using Open.IdentityServer.Events; +using Open.IdentityServer.Models; +using Open.IdentityServer.Services; +using Open.IdentityServer.Stores; +using Open.IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Validation.Setup; +using Open.IdentityServer.Validation; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Validation.Default; + +public class TokenRequestValidatorTests +{ + private static NameValueCollection PasswordGrantParameters( + string userName = "bob", string password = "bob", string scope = "resource") => + new() + { + [OidcConstants.TokenRequest.GrantType] = OidcConstants.GrantTypes.Password, + [OidcConstants.TokenRequest.UserName] = userName, + [OidcConstants.TokenRequest.Password] = password, + [OidcConstants.TokenRequest.Scope] = scope + }; + + private static IClientStore Clients { get; } = Factory.CreateClientStore(); + + // Raising internal events + + [Fact] + public async Task PasswordGrant_WhenCredentialsValidAndUserActive_RaisesUserLoginSuccessEvent() + { + var events = new TestEventService(); + var subject = Factory.CreateTokenRequestValidator(events: events); + var client = await Clients.FindEnabledClientByIdAsync("roclient"); + + // bob / bob succeeds in TestResourceOwnerPasswordValidator (username == password) + var result = await subject.ValidateRequestAsync( + PasswordGrantParameters(userName: "bob", password: "bob"), + client.ToValidationResult()); + + result.IsError.Should().BeFalse(); + + var evt = events.AssertEventWasRaised(); + evt.Username.Should().Be("bob"); + evt.SubjectId.Should().Be("bob"); // GrantValidationResult uses userName as subjectId + evt.ClientId.Should().Be("roclient"); + } + + [Fact] + public async Task PasswordGrant_WhenValidatorReturnsUnsupportedGrantType_RaisesUserLoginFailureEvent() + { + var events = new TestEventService(); + var subject = Factory.CreateTokenRequestValidator( + resourceOwnerValidator: new NotSupportedResourceOwnerPasswordValidator(TestLogger.Create()), + events: events); + + var client = await Clients.FindEnabledClientByIdAsync("roclient"); + + var result = await subject.ValidateRequestAsync( + PasswordGrantParameters(), + client.ToValidationResult()); + + result.IsError.Should().BeTrue(); + result.Error.Should().Be(OidcConstants.TokenErrors.UnsupportedGrantType); + + var evt = events.AssertEventWasRaised(); + evt.Username.Should().Be("bob"); + evt.Message.Should().Be("password grant type not supported"); + evt.ClientId.Should().Be("roclient"); + } + + [Fact] + public async Task PasswordGrant_WhenValidatorReturnsErrorWithNoDescription_RaisesUserLoginFailureEventWithDefaultDescription() + { + var events = new TestEventService(); + var subject = Factory.CreateTokenRequestValidator( + resourceOwnerValidator: new TestResourceOwnerPasswordValidator(TokenRequestErrors.InvalidGrant), + events: events); + + var client = await Clients.FindEnabledClientByIdAsync("roclient"); + + var result = await subject.ValidateRequestAsync( + PasswordGrantParameters(), + client.ToValidationResult()); + + result.IsError.Should().BeTrue(); + result.ErrorDescription.Should().Be("invalid_username_or_password"); + + var evt = events.AssertEventWasRaised(); + evt.Username.Should().Be("bob"); + evt.Message.Should().Be("invalid_username_or_password"); + evt.ClientId.Should().Be("roclient"); + } + + [Fact] + public async Task PasswordGrant_WhenValidatorReturnsErrorWithDescription_RaisesUserLoginFailureEventWithThatDescription() + { + var events = new TestEventService(); + var subject = Factory.CreateTokenRequestValidator( + resourceOwnerValidator: new TestResourceOwnerPasswordValidator( + TokenRequestErrors.InvalidGrant, "account_locked"), + events: events); + + var client = await Clients.FindEnabledClientByIdAsync("roclient"); + + var result = await subject.ValidateRequestAsync( + PasswordGrantParameters(), + client.ToValidationResult()); + + result.IsError.Should().BeTrue(); + result.ErrorDescription.Should().Be("account_locked"); + + var evt = events.AssertEventWasRaised(); + evt.Username.Should().Be("bob"); + evt.Message.Should().Be("account_locked"); + evt.ClientId.Should().Be("roclient"); + } + + [Fact] + public async Task PasswordGrant_WhenValidatorReturnsNullSubject_RaisesUserLoginFailureEvent() + { + var events = new TestEventService(); + var subject = Factory.CreateTokenRequestValidator(events: events); + var client = await Clients.FindEnabledClientByIdAsync("roclient"); + + // Mismatched credentials: TestResourceOwnerPasswordValidator leaves + // context.Result unset (default GrantValidationResult has null Subject + // and IsError == false), triggering the null-subject guard. + var result = await subject.ValidateRequestAsync( + PasswordGrantParameters(userName: "bob", password: "wrong"), + client.ToValidationResult()); + + result.IsError.Should().BeTrue(); + + var evt = events.AssertEventWasRaised(); + evt.Username.Should().Be("bob"); + evt.Message.Should().Be("invalid_username_or_password"); + evt.ClientId.Should().Be("roclient"); + } + + [Fact] + public async Task PasswordGrant_WhenUserIsInactive_RaisesUserLoginFailureEvent() + { + var events = new TestEventService(); + var subject = Factory.CreateTokenRequestValidator( + profile: new TestProfileService(shouldBeActive: false), + events: events); + + var client = await Clients.FindEnabledClientByIdAsync("roclient"); + + // bob / bob succeeds credential validation but profile says inactive + var result = await subject.ValidateRequestAsync( + PasswordGrantParameters(userName: "bob", password: "bob"), + client.ToValidationResult()); + + result.IsError.Should().BeTrue(); + result.Error.Should().Be(OidcConstants.TokenErrors.InvalidGrant); + + var evt = events.AssertEventWasRaised(); + evt.Username.Should().Be("bob"); + evt.Message.Should().Be("user is inactive"); + evt.ClientId.Should().Be("roclient"); + } + + // Emitting telemetry signals + + [Fact] + public async Task PasswordGrant_WhenCredentialsValidAndUserActive_EmitsTelemetrySignal() + { + var telemetry = new Mock(); + telemetry.Setup(t => t.CountResourceOwnerAuthentication("roclient")) + .Verifiable(Times.Once); + + var subject = Factory.CreateTokenRequestValidator(telemetry: telemetry.Object); + var client = await Clients.FindEnabledClientByIdAsync("roclient"); + + // bob / bob succeeds in TestResourceOwnerPasswordValidator (username == password) + var result = await subject.ValidateRequestAsync( + PasswordGrantParameters(userName: "bob", password: "bob"), + client.ToValidationResult()); + + result.IsError.Should().BeFalse(); + + telemetry.Verify(); + } + + [Fact] + public async Task PasswordGrant_WhenValidatorReturnsUnsupportedGrantType_EmitsTelemetrySignal() + { + var telemetry = new Mock(); + telemetry.Setup(t => t.CountResourceOwnerAuthentication( "roclient","password grant type not supported")) + .Verifiable(Times.Once); + + var subject = Factory.CreateTokenRequestValidator( + resourceOwnerValidator: new NotSupportedResourceOwnerPasswordValidator(TestLogger.Create()), + telemetry: telemetry.Object); + + var client = await Clients.FindEnabledClientByIdAsync("roclient"); + + var result = await subject.ValidateRequestAsync( + PasswordGrantParameters(), + client.ToValidationResult()); + + result.IsError.Should().BeTrue(); + + telemetry.Verify(); + } + + [Fact] + public async Task PasswordGrant_WhenValidatorReturnsErrorWithNoDescription_EmitsTelemetrySignal() + { + var telemetry = new Mock(); + telemetry.Setup(t => t.CountResourceOwnerAuthentication( "roclient", "invalid_username_or_password")) + .Verifiable(Times.Once); + + var subject = Factory.CreateTokenRequestValidator( + resourceOwnerValidator: new TestResourceOwnerPasswordValidator(TokenRequestErrors.InvalidGrant), + telemetry: telemetry.Object); + + var client = await Clients.FindEnabledClientByIdAsync("roclient"); + + var result = await subject.ValidateRequestAsync( + PasswordGrantParameters(), + client.ToValidationResult()); + + result.IsError.Should().BeTrue(); + + telemetry.Verify(); + } + + [Fact] + public async Task PasswordGrant_WhenValidatorReturnsErrorWithDescription_EmitsTelemetrySignal() + { + var telemetry = new Mock(); + telemetry.Setup(t => t.CountResourceOwnerAuthentication("roclient", "account_locked")) + .Verifiable(Times.Once); + + var subject = Factory.CreateTokenRequestValidator( + resourceOwnerValidator: new TestResourceOwnerPasswordValidator( + TokenRequestErrors.InvalidGrant, "account_locked"), + telemetry: telemetry.Object); + + var client = await Clients.FindEnabledClientByIdAsync("roclient"); + + var result = await subject.ValidateRequestAsync( + PasswordGrantParameters(), + client.ToValidationResult()); + + result.IsError.Should().BeTrue(); + + telemetry.Verify(); + } + + [Fact] + public async Task PasswordGrant_WhenValidatorReturnsNullSubject_EmitsTelemetrySignal() + { + var telemetry = new Mock(); + telemetry.Setup(t => t.CountResourceOwnerAuthentication("roclient", "invalid_username_or_password")) + .Verifiable(Times.Once); + + var subject = Factory.CreateTokenRequestValidator(telemetry: telemetry.Object); + var client = await Clients.FindEnabledClientByIdAsync("roclient"); + + // Mismatched credentials: TestResourceOwnerPasswordValidator leaves + // context.Result unset (default GrantValidationResult has null Subject + // and IsError == false), triggering the null-subject guard. + var result = await subject.ValidateRequestAsync( + PasswordGrantParameters(userName: "bob", password: "wrong"), + client.ToValidationResult()); + + result.IsError.Should().BeTrue(); + + telemetry.Verify(); + } + + [Fact] + public async Task PasswordGrant_WhenUserIsInactive_EmitsTelemetrySignal() + { + var telemetry = new Mock(); + telemetry.Setup(t => t.CountResourceOwnerAuthentication("roclient", "user is inactive")) + .Verifiable(Times.Once); + + var subject = Factory.CreateTokenRequestValidator( + profile: new TestProfileService(shouldBeActive: false), + telemetry: telemetry.Object); + + var client = await Clients.FindEnabledClientByIdAsync("roclient"); + + // bob / bob succeeds credential validation but profile says inactive + var result = await subject.ValidateRequestAsync( + PasswordGrantParameters(userName: "bob", password: "bob"), + client.ToValidationResult()); + + result.IsError.Should().BeTrue(); + + telemetry.Verify(); + } + + [Fact] + public async Task ValidateRequestAsync_WhenCalled_ShouldTrace() + { + var telemetry = new Mock(); + var trace = new Mock(); + + telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(trace.Object); + + var subject = Factory.CreateTokenRequestValidator( + profile: new TestProfileService(shouldBeActive: false), + telemetry: telemetry.Object); + + var client = await Clients.FindEnabledClientByIdAsync("roclient"); + + await subject.ValidateRequestAsync( + PasswordGrantParameters(userName: "bob", password: "bob"), + client.ToValidationResult()); + + telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Validation, subject, "ValidateRequestAsync")); + trace.Verify(t => t.Dispose(), Times.Once); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/DeviceAuthorizationRequestValidation.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/DeviceAuthorizationRequestValidation.cs index 44fcafe1a..61c34200b 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/DeviceAuthorizationRequestValidation.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/DeviceAuthorizationRequestValidation.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -7,13 +8,14 @@ using System.Linq; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Validation.Setup; -using Open.IdentityServer; +using Moq; +using Open.IdentityServer.UnitTests.Validation.Setup; using Open.IdentityServer.Models; +using Open.IdentityServer.Services; using Open.IdentityServer.Validation; using Xunit; -namespace IdentityServer.UnitTests.Validation; +namespace Open.IdentityServer.UnitTests.Validation; public class DeviceAuthorizationRequestValidation { @@ -192,4 +194,25 @@ public async Task Missing_Scopes_And_Client_Scopes_Empty() result.IsError.Should().BeTrue(); result.Error.Should().Be(OidcConstants.AuthorizeErrors.InvalidScope); } + + + [Fact] + [Trait("Category", Category)] + public async Task Starts_Telemetry_Trace() + { + Mock telemetry = new(); + Mock trace = new(); + + telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(trace.Object); + + var parameters = new NameValueCollection {{"scope", "openid"}}; + + var validator = Factory.CreateDeviceAuthorizationRequestValidator(telemetry: telemetry.Object); + await validator.ValidateAsync(parameters, new ClientSecretValidationResult {Client = testClient}); + + telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Validation, validator, "ValidateAsync")); + trace.Verify(t => t.Dispose(), Times.Once); + } } \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/DeviceCodeValidation.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/DeviceCodeValidation.cs index 68ff588cf..c51315147 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/DeviceCodeValidation.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/DeviceCodeValidation.cs @@ -1,4 +1,5 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. @@ -6,14 +7,15 @@ using System.Collections.Generic; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Validation.Setup; -using Open.IdentityServer; +using Moq; +using Open.IdentityServer.UnitTests.Validation.Setup; using Open.IdentityServer.Models; +using Open.IdentityServer.Services; using Open.IdentityServer.Stores; using Open.IdentityServer.Validation; using Xunit; -namespace IdentityServer.UnitTests.Validation; +namespace Open.IdentityServer.UnitTests.Validation; public class DeviceCodeValidation { @@ -236,4 +238,35 @@ public async Task Valid_DeviceCode() context.Result.IsError.Should().BeFalse(); } + + [Fact] + [Trait("Category", Category)] + public async Task Traces_Telemetry() + { + Mock telemetry = new(); + Mock trace = new(); + + telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(trace.Object); + + var client = await _clients.FindClientByIdAsync("device_flow"); + var service = Factory.CreateDeviceCodeService(); + + var handle = await service.StoreDeviceAuthorizationAsync(Guid.NewGuid().ToString(), deviceCode); + + var validator = Factory.CreateDeviceCodeValidator( + service, + telemetry: telemetry.Object); + + var request = new ValidatedTokenRequest(); + request.SetClient(client); + + var context = new DeviceCodeValidationContext {DeviceCode = handle, Request = request}; + + await validator.ValidateAsync(context); + + telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Validation, validator, "ValidateAsync")); + trace.Verify(t => t.Dispose(), Times.Once); + } } \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/EndSessionRequestValidation/EndSessionRequestValidatorTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/EndSessionRequestValidation/EndSessionRequestValidatorTests.cs index 7ea693144..7d01944b0 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/EndSessionRequestValidation/EndSessionRequestValidatorTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/EndSessionRequestValidation/EndSessionRequestValidatorTests.cs @@ -7,15 +7,17 @@ using System.Security.Claims; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; +using Moq; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer; using Open.IdentityServer.Configuration; using Open.IdentityServer.Extensions; using Open.IdentityServer.Models; +using Open.IdentityServer.Services; using Open.IdentityServer.Validation; using Xunit; -namespace IdentityServer.UnitTests.Validation.EndSessionRequestValidation; +namespace Open.IdentityServer.UnitTests.Validation.EndSessionRequestValidation; public class EndSessionRequestValidatorTests { @@ -27,6 +29,8 @@ public class EndSessionRequestValidatorTests private MockUserSession _userSession = new MockUserSession(); private MockLogoutNotificationService _mockLogoutNotificationService = new MockLogoutNotificationService(); private MockMessageStore _mockEndSessionMessageStore = new MockMessageStore(); + private Mock _telemetry = new(); + private Mock _trace = new(); private ClaimsPrincipal _user; @@ -43,6 +47,7 @@ public EndSessionRequestValidatorTests() _userSession, _mockLogoutNotificationService, _mockEndSessionMessageStore, + _telemetry.Object, TestLogger.Create()); } @@ -183,4 +188,48 @@ public async Task successful_request_should_return_inputs() result.IsError.Should().BeFalse(); result.ValidatedRequest.Raw.Should().BeSameAs(parameters); } + + [Fact] + public async Task ValidateAsync_WhenCalled_ShouldTraceTelemetry() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + _stubTokenValidator.IdentityTokenValidationResult = new TokenValidationResult() + { + IsError = false, + Claims = new Claim[] { new Claim("sub", _user.GetSubjectId()) }, + Client = new Client() { ClientId = "client"} + }; + _stubRedirectUriValidator.IsPostLogoutRedirectUriValid = true; + + var parameters = new NameValueCollection(); + parameters.Add("id_token_hint", "id_token"); + parameters.Add("post_logout_redirect_uri", "http://client/signout-cb"); + parameters.Add("client_id", "client1"); + parameters.Add("state", "foo"); + + await _subject.ValidateAsync(parameters, _user); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Validation, _subject, "ValidateAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } + + [Fact] + public async Task ValidateCallbackAsync_WhenCalled_ShouldTraceTelemetry() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var parameters = new NameValueCollection(); + + parameters[Constants.UIConstants.DefaultRoutePathParams.EndSessionCallback] = "end session callback"; + + await _subject.ValidateCallbackAsync(parameters); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Validation, _subject, "ValidateCallbackAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } } \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/EndSessionRequestValidation/StubRedirectUriValidator.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/EndSessionRequestValidation/StubRedirectUriValidator.cs index 4159ba93b..2fb171d4d 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/EndSessionRequestValidation/StubRedirectUriValidator.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/EndSessionRequestValidation/StubRedirectUriValidator.cs @@ -6,7 +6,7 @@ using Open.IdentityServer.Models; using Open.IdentityServer.Validation; -namespace IdentityServer.UnitTests.Validation.EndSessionRequestValidation; +namespace Open.IdentityServer.UnitTests.Validation.EndSessionRequestValidation; public class StubRedirectUriValidator : IRedirectUriValidator { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/EndSessionRequestValidation/StubTokenValidator.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/EndSessionRequestValidation/StubTokenValidator.cs index 09caa8c9a..40670fc98 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/EndSessionRequestValidation/StubTokenValidator.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/EndSessionRequestValidation/StubTokenValidator.cs @@ -6,7 +6,7 @@ using Open.IdentityServer.Models; using Open.IdentityServer.Validation; -namespace IdentityServer.UnitTests.Validation.EndSessionRequestValidation; +namespace Open.IdentityServer.UnitTests.Validation.EndSessionRequestValidation; public class StubTokenValidator : ITokenValidator { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/GrantTypesValidation.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/GrantTypesValidation.cs index 76a4aa34a..a80ceb277 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/GrantTypesValidation.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/GrantTypesValidation.cs @@ -8,7 +8,7 @@ using Open.IdentityServer.Models; using Xunit; -namespace IdentityServer.UnitTests.Validation; +namespace Open.IdentityServer.UnitTests.Validation; public class GrantTypesValidation { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/IdentityTokenValidation.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/IdentityTokenValidation.cs index a0fa4dcd3..764cd60e0 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/IdentityTokenValidation.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/IdentityTokenValidation.cs @@ -1,15 +1,17 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. using System.IdentityModel.Tokens.Jwt; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Validation.Setup; -using Open.IdentityServer; +using Moq; +using Open.IdentityServer.Services; +using Open.IdentityServer.UnitTests.Validation.Setup; using Xunit; -namespace IdentityServer.UnitTests.Validation; +namespace Open.IdentityServer.UnitTests.Validation; public class IdentityTokenValidation { @@ -83,4 +85,26 @@ public async Task IdentityToken_Too_Long() result.IsError.Should().BeTrue(); result.Error.Should().Be(OidcConstants.ProtectedResourceErrors.InvalidToken); } + + [Fact] + [Trait("Category", Category)] + public async Task IdentityToken_ShouldTraceTelemetry() + { + var telemetry = new Mock(); + var trace = new Mock(); + + telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(trace.Object); + + var creator = Factory.CreateDefaultTokenCreator(); + var token = TokenFactory.CreateIdentityToken("roclient", "valid"); + var jwt = await creator.CreateTokenAsync(token); + + var validator = Factory.CreateTokenValidator(telemetry: telemetry.Object); + await validator.ValidateIdentityTokenAsync(jwt, "roclient"); + + telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Validation, validator, "ValidateIdentityTokenAsync")); + trace.Verify(t => t.Dispose(), Times.Once); + } } \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/IntrospectionRequestValidatorTests.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/IntrospectionRequestValidatorTests.cs index 89503b1dc..5e79347ea 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/IntrospectionRequestValidatorTests.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/IntrospectionRequestValidatorTests.cs @@ -7,14 +7,14 @@ using System.Linq; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; -using IdentityServer.UnitTests.Validation.Setup; +using Open.IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Validation.Setup; using Open.IdentityServer.Models; using Open.IdentityServer.Stores; using Open.IdentityServer.Validation; using Xunit; -namespace IdentityServer.UnitTests.Validation; +namespace Open.IdentityServer.UnitTests.Validation; public class IntrospectionRequestValidatorTests { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/ResourceValidation.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/ResourceValidation.cs index f2e0dc9d7..371ea125c 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/ResourceValidation.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/ResourceValidation.cs @@ -6,13 +6,13 @@ using System.Linq; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Validation.Setup; +using Open.IdentityServer.UnitTests.Validation.Setup; using Open.IdentityServer.Extensions; using Open.IdentityServer.Models; using Open.IdentityServer.Stores; using Xunit; -namespace IdentityServer.UnitTests.Validation; +namespace Open.IdentityServer.UnitTests.Validation; public class ResourceValidation { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/ResponseTypeEqualityComparison.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/ResponseTypeEqualityComparison.cs index c97ead4dc..17cb735db 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/ResponseTypeEqualityComparison.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/ResponseTypeEqualityComparison.cs @@ -6,7 +6,7 @@ using Open.IdentityServer.Validation; using Xunit; -namespace IdentityServer.UnitTests.Validation; +namespace Open.IdentityServer.UnitTests.Validation; /// /// Tests for ResponseTypeEqualityComparer diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/RevocationRequestValidation.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/RevocationRequestValidation.cs index 202050596..6312644f4 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/RevocationRequestValidation.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/RevocationRequestValidation.cs @@ -6,13 +6,13 @@ using System.Collections.Specialized; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer; using Open.IdentityServer.Models; using Open.IdentityServer.Validation; using Xunit; -namespace IdentityServer.UnitTests.Validation; +namespace Open.IdentityServer.UnitTests.Validation; public class RevocationRequestValidation { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/ApiSecretValidation.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/ApiSecretValidation.cs new file mode 100644 index 000000000..54883f217 --- /dev/null +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/ApiSecretValidation.cs @@ -0,0 +1,311 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Collections.Generic; +using System.Threading.Tasks; +using AwesomeAssertions; +using Open.IdentityServer.UnitTests.Common; +using Microsoft.AspNetCore.Http; +using Moq; +using Open.IdentityServer; +using Open.IdentityServer.Events; +using Open.IdentityServer.Models; +using Open.IdentityServer.Services; +using Open.IdentityServer.Stores; +using Open.IdentityServer.Validation; +using Xunit; + +namespace Open.IdentityServer.UnitTests.Validation.Secrets; + +public class ApiSecretValidation +{ + private const string Category = "API Secret Validation"; + + private readonly Mock _resourceStore; + private readonly Mock _parser; + private readonly Mock _validator; + private readonly Mock _telemetry; + private readonly Mock _trace; + private readonly TestEventService _events; + private readonly ApiSecretValidator _subject; + + public ApiSecretValidation() + { + _resourceStore = new Mock(); + _parser = new Mock(); + _validator = new Mock(); + _telemetry = new Mock(); + _trace = new Mock(); + _events = new TestEventService(); + + _subject = new ApiSecretValidator( + _resourceStore.Object, + _parser.Object, + _validator.Object, + _events, + _telemetry.Object, + TestLogger.Create()); + } + + [Fact] + [Trait("Category", Category)] + public async Task no_secret_found_should_fail() + { + _parser.Setup(x => x.ParseAsync(It.IsAny())) + .ReturnsAsync((ParsedSecret)null); + + var result = await _subject.ValidateAsync(new DefaultHttpContext()); + + result.IsError.Should().BeTrue(); + result.Resource.Should().BeNull(); + _events.AssertEventWasRaised(); + } + + [Fact] + [Trait("Category", Category)] + public async Task no_secret_found_should_emit_telemetry_count() + { + _telemetry.Setup(x => x.CountApiSecretValidation("unknown", null, "No API id or secret found")) + .Verifiable(Times.Once); + + _parser.Setup(x => x.ParseAsync(It.IsAny())) + .ReturnsAsync((ParsedSecret)null); + + await _subject.ValidateAsync(new DefaultHttpContext()); + + _telemetry.Verify(); + } + + [Fact] + [Trait("Category", Category)] + public async Task unknown_api_resource_should_fail() + { + _parser.Setup(x => x.ParseAsync(It.IsAny())) + .ReturnsAsync(new ParsedSecret { Id = "unknown_api", Type = "SharedSecret" }); + + _resourceStore.Setup(x => x.FindApiResourcesByNameAsync(It.IsAny>())) + .ReturnsAsync(new List()); + + var result = await _subject.ValidateAsync(new DefaultHttpContext()); + + result.IsError.Should().BeTrue(); + result.Resource.Should().BeNull(); + _events.AssertEventWasRaised(); + } + + [Fact] + [Trait("Category", Category)] + public async Task unknown_api_resource_should_emit_telemetry_signal() + { + _telemetry.Setup(x => x.CountApiSecretValidation("unknown_api", null, "Unknown API resource")) + .Verifiable(Times.Once); + + _parser.Setup(x => x.ParseAsync(It.IsAny())) + .ReturnsAsync(new ParsedSecret { Id = "unknown_api", Type = "SharedSecret" }); + + _resourceStore.Setup(x => x.FindApiResourcesByNameAsync(It.IsAny>())) + .ReturnsAsync(new List()); + + await _subject.ValidateAsync(new DefaultHttpContext()); + + _telemetry.Verify(); + } + + [Fact] + [Trait("Category", Category)] + public async Task multiple_api_resources_should_fail() + { + _parser.Setup(x => x.ParseAsync(It.IsAny())) + .ReturnsAsync(new ParsedSecret { Id = "api_id", Type = "SharedSecret" }); + + _resourceStore.Setup(x => x.FindApiResourcesByNameAsync(It.IsAny>())) + .ReturnsAsync(new List() + { + new ApiResource("api_id"), + new ApiResource("api_id"), + }); + + var result = await _subject.ValidateAsync(new DefaultHttpContext()); + + result.IsError.Should().BeTrue(); + result.Resource.Should().BeNull(); + _events.AssertEventWasRaised(); + } + + [Fact] + [Trait("Category", Category)] + public async Task multiple_api_resources_should_emit_telemetry_signal() + { + _telemetry.Setup(x => x.CountApiSecretValidation("api_id", null,"Invalid API resource")) + .Verifiable(Times.Once); + + _parser.Setup(x => x.ParseAsync(It.IsAny())) + .ReturnsAsync(new ParsedSecret { Id = "api_id", Type = "SharedSecret" }); + + _resourceStore.Setup(x => x.FindApiResourcesByNameAsync(It.IsAny>())) + .ReturnsAsync(new List() + { + new ApiResource("api_id"), + new ApiResource("api_id"), + }); + + await _subject.ValidateAsync(new DefaultHttpContext()); + + _telemetry.Verify(); + } + + [Fact] + [Trait("Category", Category)] + public async Task disabled_api_resource_should_fail() + { + var api = new ApiResource("my_api") { Enabled = false }; + + _parser.Setup(x => x.ParseAsync(It.IsAny())) + .ReturnsAsync(new ParsedSecret { Id = "my_api", Type = "SharedSecret" }); + + _resourceStore.Setup(x => x.FindApiResourcesByNameAsync(It.IsAny>())) + .ReturnsAsync(new List { api }); + + var result = await _subject.ValidateAsync(new DefaultHttpContext()); + + result.IsError.Should().BeTrue(); + result.Resource.Should().BeNull(); + _events.AssertEventWasRaised(); + } + + [Fact] + [Trait("Category", Category)] + public async Task disabled_api_resource_should_emit_telemetry_signal() + { + _telemetry.Setup(x => x.CountApiSecretValidation("my_api", null, "API resource not enabled")) + .Verifiable(Times.Once); + + var api = new ApiResource("my_api") { Enabled = false }; + + _parser.Setup(x => x.ParseAsync(It.IsAny())) + .ReturnsAsync(new ParsedSecret { Id = "my_api", Type = "SharedSecret" }); + + _resourceStore.Setup(x => x.FindApiResourcesByNameAsync(It.IsAny>())) + .ReturnsAsync(new List { api }); + + await _subject.ValidateAsync(new DefaultHttpContext()); + + _telemetry.Verify(); + } + + [Fact] + [Trait("Category", Category)] + public async Task invalid_secret_should_fail() + { + var api = new ApiResource("my_api"); + + _parser.Setup(x => x.ParseAsync(It.IsAny())) + .ReturnsAsync(new ParsedSecret { Id = "my_api", Type = "SharedSecret" }); + + _resourceStore.Setup(x => x.FindApiResourcesByNameAsync(It.IsAny>())) + .ReturnsAsync(new List { api }); + + _validator.Setup(x => x.ValidateAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(new SecretValidationResult { Success = false }); + + var result = await _subject.ValidateAsync(new DefaultHttpContext()); + + result.IsError.Should().BeTrue(); + result.Resource.Should().BeNull(); + _events.AssertEventWasRaised(); + } + + [Fact] + [Trait("Category", Category)] + public async Task invalid_secret_should_emit_telemetry_signal() + { + _telemetry.Setup(x => x.CountApiSecretValidation("my_api", null, "Invalid API secret")) + .Verifiable(Times.Once); + + var api = new ApiResource("my_api"); + + _parser.Setup(x => x.ParseAsync(It.IsAny())) + .ReturnsAsync(new ParsedSecret { Id = "my_api", Type = "SharedSecret" }); + + _resourceStore.Setup(x => x.FindApiResourcesByNameAsync(It.IsAny>())) + .ReturnsAsync(new List { api }); + + _validator.Setup(x => x.ValidateAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(new SecretValidationResult { Success = false }); + + await _subject.ValidateAsync(new DefaultHttpContext()); + + _telemetry.Verify(); + } + + [Fact] + [Trait("Category", Category)] + public async Task valid_secret_should_succeed() + { + var api = new ApiResource("my_api"); + + _parser.Setup(x => x.ParseAsync(It.IsAny())) + .ReturnsAsync(new ParsedSecret { Id = "my_api", Type = "SharedSecret" }); + + _resourceStore.Setup(x => x.FindApiResourcesByNameAsync(It.IsAny>())) + .ReturnsAsync(new List { api }); + + _validator.Setup(x => x.ValidateAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(new SecretValidationResult { Success = true }); + + var result = await _subject.ValidateAsync(new DefaultHttpContext()); + + result.IsError.Should().BeFalse(); + result.Resource.Should().NotBeNull(); + result.Resource.Name.Should().Be("my_api"); + _events.AssertEventWasRaised(); + } + + [Fact] + [Trait("Category", Category)] + public async Task valid_secret_should_emit_telemetry_signal() + { + _telemetry.Setup(x => x.CountApiSecretValidation("my_api", "SharedSecret")) + .Verifiable(Times.Once); + + var api = new ApiResource("my_api"); + + _parser.Setup(x => x.ParseAsync(It.IsAny())) + .ReturnsAsync(new ParsedSecret { Id = "my_api", Type = "SharedSecret" }); + + _resourceStore.Setup(x => x.FindApiResourcesByNameAsync(It.IsAny>())) + .ReturnsAsync(new List { api }); + + _validator.Setup(x => x.ValidateAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(new SecretValidationResult { Success = true }); + + await _subject.ValidateAsync(new DefaultHttpContext()); + + _telemetry.Verify(); + } + + [Fact] + [Trait("Category", Category)] + public async Task validate_should_initiate_telemetry_trace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var api = new ApiResource("my_api"); + + _parser.Setup(x => x.ParseAsync(It.IsAny())) + .ReturnsAsync(new ParsedSecret { Id = "my_api", Type = "SharedSecret" }); + + _resourceStore.Setup(x => x.FindApiResourcesByNameAsync(It.IsAny>())) + .ReturnsAsync(new List { api }); + + _validator.Setup(x => x.ValidateAsync(It.IsAny>(), It.IsAny())) + .ReturnsAsync(new SecretValidationResult { Success = true }); + + await _subject.ValidateAsync(new DefaultHttpContext()); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Validation, _subject, "ValidateAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } +} \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/BasicAuthenticationCredentialParsing.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/BasicAuthenticationCredentialParsing.cs index a9e3dab06..409d156e5 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/BasicAuthenticationCredentialParsing.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/BasicAuthenticationCredentialParsing.cs @@ -7,7 +7,7 @@ using System.Text; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer; using Open.IdentityServer.Configuration; using Open.IdentityServer.Validation; @@ -15,7 +15,7 @@ using Microsoft.Extensions.Primitives; using Xunit; -namespace IdentityServer.UnitTests.Validation.Secrets; +namespace Open.IdentityServer.UnitTests.Validation.Secrets; [SuppressMessage( "Usage", diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/ClientAssertionSecretParsing.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/ClientAssertionSecretParsing.cs index a4ff32102..88ac384ed 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/ClientAssertionSecretParsing.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/ClientAssertionSecretParsing.cs @@ -8,7 +8,7 @@ using System.Text; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer; using Open.IdentityServer.Configuration; using Open.IdentityServer.Validation; @@ -16,7 +16,7 @@ using Microsoft.Extensions.Logging; using Xunit; -namespace IdentityServer.UnitTests.Validation.Secrets; +namespace Open.IdentityServer.UnitTests.Validation.Secrets; public class ClientAssertionSecretParsing { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/ClientSecretValidation.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/ClientSecretValidation.cs index 4735930e1..6d4ff1055 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/ClientSecretValidation.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/ClientSecretValidation.cs @@ -6,11 +6,11 @@ using System.Text; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Validation.Setup; +using Open.IdentityServer.UnitTests.Validation.Setup; using Microsoft.AspNetCore.Http; using Xunit; -namespace IdentityServer.UnitTests.Validation.Secrets; +namespace Open.IdentityServer.UnitTests.Validation.Secrets; public class ClientSecretValidation { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/FormPostCredentialParsing.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/FormPostCredentialParsing.cs index 092075a54..3070f1780 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/FormPostCredentialParsing.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/FormPostCredentialParsing.cs @@ -6,7 +6,7 @@ using System.Text; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer; using Open.IdentityServer.Configuration; using Open.IdentityServer.Validation; @@ -14,7 +14,7 @@ using Microsoft.Extensions.Logging; using Xunit; -namespace IdentityServer.UnitTests.Validation.Secrets; +namespace Open.IdentityServer.UnitTests.Validation.Secrets; public class FormPostCredentialExtraction { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/HashedSharedSecretValidation.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/HashedSharedSecretValidation.cs index 7d8abd643..72ce65e4a 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/HashedSharedSecretValidation.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/HashedSharedSecretValidation.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Validation.Setup; +using Open.IdentityServer.UnitTests.Validation.Setup; using Open.IdentityServer; using Open.IdentityServer.Models; using Open.IdentityServer.Stores; @@ -12,7 +12,7 @@ using Microsoft.Extensions.Logging; using Xunit; -namespace IdentityServer.UnitTests.Validation.Secrets; +namespace Open.IdentityServer.UnitTests.Validation.Secrets; public class HashedSharedSecretValidation { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/MutualTlsSecretValidation.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/MutualTlsSecretValidation.cs index 9b9192ade..33eda93f4 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/MutualTlsSecretValidation.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/MutualTlsSecretValidation.cs @@ -5,8 +5,8 @@ using System; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; -using IdentityServer.UnitTests.Validation.Setup; +using Open.IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Validation.Setup; using Open.IdentityServer; using Open.IdentityServer.Models; using Open.IdentityServer.Stores; @@ -14,7 +14,7 @@ using Microsoft.Extensions.Logging; using Xunit; -namespace IdentityServer.UnitTests.Validation.Secrets; +namespace Open.IdentityServer.UnitTests.Validation.Secrets; public class MutualTlsSecretValidation { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/PlainTextClientSecretValidation.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/PlainTextClientSecretValidation.cs index da30fb41a..a1e8a03ac 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/PlainTextClientSecretValidation.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/PlainTextClientSecretValidation.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Validation.Setup; +using Open.IdentityServer.UnitTests.Validation.Setup; using Open.IdentityServer; using Open.IdentityServer.Models; using Open.IdentityServer.Stores; @@ -12,7 +12,7 @@ using Microsoft.Extensions.Logging; using Xunit; -namespace IdentityServer.UnitTests.Validation.Secrets; +namespace Open.IdentityServer.UnitTests.Validation.Secrets; public class PlainTextClientSecretValidation { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/PrivateKeyJwtSecretValidation.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/PrivateKeyJwtSecretValidation.cs index eaedfd9ae..80d9d4e46 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/PrivateKeyJwtSecretValidation.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/PrivateKeyJwtSecretValidation.cs @@ -8,9 +8,9 @@ using System.Security.Claims; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; -using IdentityServer.UnitTests.Services.Default; -using IdentityServer.UnitTests.Validation.Setup; +using Open.IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Services.Default; +using Open.IdentityServer.UnitTests.Validation.Setup; using Open.IdentityServer; using Open.IdentityServer.Configuration; using Open.IdentityServer.Models; @@ -19,9 +19,10 @@ using Open.IdentityServer.Validation; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; +using Moq; using Xunit; -namespace IdentityServer.UnitTests.Validation.Secrets; +namespace Open.IdentityServer.UnitTests.Validation.Secrets; public class PrivateKeyJwtSecretValidation { @@ -37,7 +38,7 @@ public PrivateKeyJwtSecretValidation() IssuerUri = "https://idsrv3.com" } ), - new DefaultReplayCache(new TestCache()), + new DefaultReplayCache(new TestCache(), Mock.Of()), new LoggerFactory().CreateLogger() ); _clients = new InMemoryClientStore(ClientValidationTestClients.Get()); diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/SecretValidation.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/SecretValidation.cs index a248907db..aec58694a 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/SecretValidation.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Secrets/SecretValidation.cs @@ -4,9 +4,8 @@ using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; -using IdentityServer.UnitTests.Validation.Setup; -using Open.IdentityServer; +using Open.IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Validation.Setup; using Open.IdentityServer.Configuration; using Open.IdentityServer.Models; using Open.IdentityServer.Stores; @@ -14,7 +13,7 @@ using Microsoft.Extensions.Logging; using Xunit; -namespace IdentityServer.UnitTests.Validation.Secrets; +namespace Open.IdentityServer.UnitTests.Validation.Secrets; public class SecretValidation { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/ClientValidationTestClients.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/ClientValidationTestClients.cs index e645ec4e2..3aa808ed9 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/ClientValidationTestClients.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/ClientValidationTestClients.cs @@ -5,12 +5,12 @@ using System; using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; -using IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer; using Open.IdentityServer.Models; using static Open.IdentityServer.IdentityServerConstants; -namespace IdentityServer.UnitTests.Validation.Setup; +namespace Open.IdentityServer.UnitTests.Validation.Setup; internal static class ClientValidationTestClients { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/Factory.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/Factory.cs index bf6934a80..298e22da5 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/Factory.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/Factory.cs @@ -1,12 +1,14 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Net.Http; -using IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer.Configuration; using Open.IdentityServer.Models; using Open.IdentityServer.Services; @@ -16,7 +18,7 @@ using Open.IdentityServer.Validation; using Microsoft.Extensions.Logging; -namespace IdentityServer.UnitTests.Validation.Setup; +namespace Open.IdentityServer.UnitTests.Validation.Setup; internal static class Factory { @@ -37,7 +39,9 @@ public static TokenRequestValidator CreateTokenRequestValidator( ICustomTokenRequestValidator customRequestValidator = null, ITokenValidator tokenValidator = null, IRefreshTokenService refreshTokenService = null, - IResourceValidator resourceValidator = null) + IResourceValidator resourceValidator = null, + IEventService events = null, + ITelemetryService telemetry = null) { if (options == null) { @@ -106,6 +110,16 @@ public static TokenRequestValidator CreateTokenRequestValidator( profile); } + if (events == null) + { + events = new TestEventService(); + } + + if (telemetry == null) + { + telemetry = new NopTelemetryService(); + } + return new TokenRequestValidator( options, authorizationCodeStore, @@ -118,8 +132,9 @@ public static TokenRequestValidator CreateTokenRequestValidator( resourceStore, tokenValidator, refreshTokenService, - new TestEventService(), + events, new StubClock(), + telemetry, TestLogger.Create()); } @@ -129,6 +144,7 @@ private static IRefreshTokenService CreateRefreshTokenService(IRefreshTokenStore store, profile, new StubClock(), + new NopTelemetryService(), TestLogger.Create()); return service; @@ -137,7 +153,8 @@ private static IRefreshTokenService CreateRefreshTokenService(IRefreshTokenStore internal static IResourceValidator CreateResourceValidator(IResourceStore store = null) { store = store ?? new InMemoryResourcesStore(TestScopes.GetIdentity(), TestScopes.GetApis(), TestScopes.GetScopes()); - return new DefaultResourceValidator(store, new DefaultScopeParser(TestLogger.Create()), TestLogger.Create()); + return new DefaultResourceValidator(store, + new DefaultScopeParser(TestLogger.Create()), new NopTelemetryService(), TestLogger.Create()); } internal static ITokenCreationService CreateDefaultTokenCreator(IdentityServerOptions options = null) @@ -145,15 +162,18 @@ internal static ITokenCreationService CreateDefaultTokenCreator(IdentityServerOp return new DefaultTokenCreationService( new StubClock(), new DefaultKeyMaterialService(new IValidationKeysStore[] { }, - new ISigningCredentialStore[] { new InMemorySigningCredentialsStore(TestCert.LoadSigningCredentials()) }), + new ISigningCredentialStore[] { new InMemorySigningCredentialsStore(TestCert.LoadSigningCredentials()) }, + new NopTelemetryService()), options ?? TestIdentityServerOptions.Create(), + new DefaultTelemetryService(), TestLogger.Create()); } public static DeviceAuthorizationRequestValidator CreateDeviceAuthorizationRequestValidator( IdentityServerOptions options = null, IResourceStore resourceStore = null, - IResourceValidator resourceValidator = null) + IResourceValidator resourceValidator = null, + ITelemetryService telemetry = null) { if (options == null) { @@ -169,11 +189,13 @@ public static DeviceAuthorizationRequestValidator CreateDeviceAuthorizationReque { resourceValidator = CreateResourceValidator(resourceStore); } - + + if (telemetry == null) telemetry = new NopTelemetryService(); return new DeviceAuthorizationRequestValidator( options, resourceValidator, + telemetry, TestLogger.Create()); } @@ -186,7 +208,8 @@ public static AuthorizeRequestValidator CreateAuthorizeRequestValidator( IRedirectUriValidator uriValidator = null, IResourceValidator resourceValidator = null, JwtRequestValidator jwtRequestValidator = null, - IJwtRequestUriHttpClient jwtRequestUriHttpClient = null) + IJwtRequestUriHttpClient jwtRequestUriHttpClient = null, + ITelemetryService telemetry = null) { if (options == null) { @@ -225,9 +248,17 @@ public static AuthorizeRequestValidator CreateAuthorizeRequestValidator( if (jwtRequestUriHttpClient == null) { - jwtRequestUriHttpClient = new DefaultJwtRequestUriHttpClient(new HttpClient(new NetworkHandler(new Exception("no jwt request uri response configured"))), options, new LoggerFactory()); + jwtRequestUriHttpClient = new DefaultJwtRequestUriHttpClient( + new HttpClient(new NetworkHandler(new Exception("no jwt request uri response configured"))), + options, + new NopTelemetryService(), + new LoggerFactory()); } + if (telemetry == null) + { + telemetry = new NopTelemetryService(); + } var userSession = new MockUserSession(); @@ -240,6 +271,7 @@ public static AuthorizeRequestValidator CreateAuthorizeRequestValidator( userSession, jwtRequestValidator, jwtRequestUriHttpClient, + telemetry, TestLogger.Create()); } @@ -247,7 +279,9 @@ public static TokenValidator CreateTokenValidator( IReferenceTokenStore store = null, IRefreshTokenStore refreshTokenStore = null, IProfileService profile = null, - IdentityServerOptions options = null, TimeProvider clock = null) + IdentityServerOptions options = null, + TimeProvider clock = null, + ITelemetryService telemetry = null) { if (options == null) { @@ -270,6 +304,8 @@ public static TokenValidator CreateTokenValidator( { refreshTokenStore = CreateRefreshTokenStore(); } + + telemetry = telemetry ?? new NopTelemetryService(); var clients = CreateClientStore(); var context = new MockHttpContextAccessor(options); @@ -288,10 +324,14 @@ public static TokenValidator CreateTokenValidator( referenceTokenStore: store, refreshTokenStore: refreshTokenStore, customValidator: new DefaultCustomTokenValidator(), - keys: new DefaultKeyMaterialService(new[] { new InMemoryValidationKeysStore(new[] { keyInfo }) }, Enumerable.Empty()), + keys: new DefaultKeyMaterialService( + new[] { new InMemoryValidationKeysStore(new[] { keyInfo }) }, + Enumerable.Empty(), + new NopTelemetryService()), logger: logger, options: options, - context: context); + context: context, + telemetry: telemetry); return validator; } @@ -300,18 +340,20 @@ public static IDeviceCodeValidator CreateDeviceCodeValidator( IDeviceFlowCodeService service, IProfileService profile = null, IDeviceFlowThrottlingService throttlingService = null, - TimeProvider clock = null) + TimeProvider clock = null, + ITelemetryService telemetry = null) { profile = profile ?? new TestProfileService(); throttlingService = throttlingService ?? new TestDeviceFlowThrottlingService(); clock = clock ?? new StubClock(); + telemetry = telemetry ?? new NopTelemetryService(); - var validator = new DeviceCodeValidator(service, profile, throttlingService, clock, TestLogger.Create()); + var validator = new DeviceCodeValidator(service, profile, throttlingService, clock, telemetry, TestLogger.Create()); return validator; } - public static IClientSecretValidator CreateClientSecretValidator(IClientStore clients = null, SecretParser parser = null, SecretValidator validator = null, IdentityServerOptions options = null) + public static IClientSecretValidator CreateClientSecretValidator(IClientStore clients = null, SecretParser parser = null, SecretValidator validator = null, IdentityServerOptions options = null, ITelemetryService telemetry = null) { options = options ?? TestIdentityServerOptions.Create(); @@ -339,7 +381,12 @@ public static IClientSecretValidator CreateClientSecretValidator(IClientStore cl validator = new SecretValidator(new StubClock(), validators, TestLogger.Create()); } - return new ClientSecretValidator(clients, parser, validator, new TestEventService(), TestLogger.Create()); + if (telemetry == null) + { + telemetry = new NopTelemetryService(); + } + + return new ClientSecretValidator(clients, parser, validator, new TestEventService(), telemetry, TestLogger.Create()); } public static IAuthorizationCodeStore CreateAuthorizationCodeStore() @@ -347,6 +394,7 @@ public static IAuthorizationCodeStore CreateAuthorizationCodeStore() return new DefaultAuthorizationCodeStore(new InMemoryPersistedGrantStore(), new PersistentGrantSerializer(), new DefaultHandleGenerationService(), + new NopTelemetryService(), TestLogger.Create()); } @@ -355,6 +403,7 @@ public static IRefreshTokenStore CreateRefreshTokenStore() return new DefaultRefreshTokenStore(new InMemoryPersistedGrantStore(), new PersistentGrantSerializer(), new DefaultHandleGenerationService(), + new NopTelemetryService(), TestLogger.Create()); } @@ -363,12 +412,13 @@ public static IReferenceTokenStore CreateReferenceTokenStore() return new DefaultReferenceTokenStore(new InMemoryPersistedGrantStore(), new PersistentGrantSerializer(), new DefaultHandleGenerationService(), + new NopTelemetryService(), TestLogger.Create()); } public static IDeviceFlowCodeService CreateDeviceCodeService() { - return new DefaultDeviceFlowCodeService(new InMemoryDeviceFlowStore(), new DefaultHandleGenerationService()); + return new DefaultDeviceFlowCodeService(new InMemoryDeviceFlowStore(), new DefaultHandleGenerationService(), new NopTelemetryService()); } public static IUserConsentStore CreateUserConsentStore() @@ -376,6 +426,77 @@ public static IUserConsentStore CreateUserConsentStore() return new DefaultUserConsentStore(new InMemoryPersistedGrantStore(), new PersistentGrantSerializer(), new DefaultHandleGenerationService(), + new NopTelemetryService(), TestLogger.Create()); } +} + +internal class NopTelemetryService : ITelemetryService +{ + public void CountInternalError(string error) + { + } + + public IDisposable BeginActiveRequest(string endpoint, string path) + { + return new NopDisposable(); + } + + public void CountApiSecretValidation(string client, string authMethod = null, string error = null) + { + } + + public void CountBackchannelAuthentication(string client, string error = null) + { + } + + public void CountClientConfigValidation(string client, string error = null) + { + } + + public void CountClientSecretValidation(string client, string authMethod = null, string error = null) + { + } + + public void CountDeviceAuthentication(string client, string error = null) + { + } + + public void CountTokenIntrospection(string caller, bool? active = null, string error = null) + { + } + + public void CountPushedAuthorizationRequest(string client, string error = null) + { + } + + public void CountResourceOwnerAuthentication(string client, string error = null) + { + } + + public void CountTokenRevocation(string client, string error = null) + { + } + + public void CountTokenIssued(string client, string grantType, string error = null) + { + } + + public ITrace Trace(string category, string activityName) + { + return null; + } + + public ITrace Trace(string category, object caller, string callingMethod = null) + { + return null; + } + + private class NopDisposable : IDisposable + { + public void Dispose() + { + + } + } } \ No newline at end of file diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TestClients.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TestClients.cs index a032ef93a..420426420 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TestClients.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TestClients.cs @@ -6,7 +6,7 @@ using Open.IdentityServer; using Open.IdentityServer.Models; -namespace IdentityServer.UnitTests.Validation.Setup; +namespace Open.IdentityServer.UnitTests.Validation.Setup; internal class TestClients { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TestDeviceCodeValidator.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TestDeviceCodeValidator.cs index 5a2854b54..a3ca71bf7 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TestDeviceCodeValidator.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TestDeviceCodeValidator.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using Open.IdentityServer.Validation; -namespace IdentityServer.UnitTests.Validation.Setup; +namespace Open.IdentityServer.UnitTests.Validation.Setup; public class TestDeviceCodeValidator : IDeviceCodeValidator { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TestDeviceFlowThrottlingService.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TestDeviceFlowThrottlingService.cs index 85d1277b1..7e87a83e0 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TestDeviceFlowThrottlingService.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TestDeviceFlowThrottlingService.cs @@ -5,7 +5,7 @@ using Open.IdentityServer.Models; using Open.IdentityServer.Services; -namespace IdentityServer.UnitTests.Validation.Setup; +namespace Open.IdentityServer.UnitTests.Validation.Setup; public class TestDeviceFlowThrottlingService : IDeviceFlowThrottlingService { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TestGrantValidator.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TestGrantValidator.cs index 0da19c0a2..1bd5522c7 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TestGrantValidator.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TestGrantValidator.cs @@ -6,7 +6,7 @@ using Open.IdentityServer.Models; using Open.IdentityServer.Validation; -namespace IdentityServer.UnitTests.Validation.Setup; +namespace Open.IdentityServer.UnitTests.Validation.Setup; internal class TestGrantValidator : IExtensionGrantValidator { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TestProfileService.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TestProfileService.cs index d872ad082..49e9b0fc4 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TestProfileService.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TestProfileService.cs @@ -6,7 +6,7 @@ using Open.IdentityServer.Models; using Open.IdentityServer.Services; -namespace IdentityServer.UnitTests.Validation.Setup; +namespace Open.IdentityServer.UnitTests.Validation.Setup; internal class TestProfileService : IProfileService { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TestResourceOwnerPasswordValidator.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TestResourceOwnerPasswordValidator.cs index 49ce18ef4..33d10ea53 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TestResourceOwnerPasswordValidator.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TestResourceOwnerPasswordValidator.cs @@ -6,7 +6,7 @@ using Open.IdentityServer.Models; using Open.IdentityServer.Validation; -namespace IdentityServer.UnitTests.Validation.Setup; +namespace Open.IdentityServer.UnitTests.Validation.Setup; public class TestResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TestScopes.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TestScopes.cs index 6bd663e7f..9b35ebf55 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TestScopes.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TestScopes.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using Open.IdentityServer.Models; -namespace IdentityServer.UnitTests.Validation.Setup; +namespace Open.IdentityServer.UnitTests.Validation.Setup; internal class TestScopes { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TestTokenValidator.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TestTokenValidator.cs index 814436999..7c5542625 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TestTokenValidator.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TestTokenValidator.cs @@ -5,7 +5,7 @@ using Open.IdentityServer.Validation; using System.Threading.Tasks; -namespace IdentityServer.UnitTests.Validation.Setup; +namespace Open.IdentityServer.UnitTests.Validation.Setup; class TestTokenValidator : ITokenValidator { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TokenFactory.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TokenFactory.cs index d58b03706..71a9f9cc7 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TokenFactory.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/TokenFactory.cs @@ -6,11 +6,10 @@ using System.Collections.Generic; using System.Linq; using System.Security.Claims; -using IdentityServer.UnitTests.Common; -using Open.IdentityServer; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer.Models; -namespace IdentityServer.UnitTests.Validation.Setup; +namespace Open.IdentityServer.UnitTests.Validation.Setup; internal static class TokenFactory { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/ValidationExtensions.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/ValidationExtensions.cs index 4af91a174..3b8a885e4 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/ValidationExtensions.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/Setup/ValidationExtensions.cs @@ -4,7 +4,7 @@ using Open.IdentityServer.Models; using Open.IdentityServer.Validation; -namespace IdentityServer.UnitTests.Validation.Setup; +namespace Open.IdentityServer.UnitTests.Validation.Setup; public static class ValidationExtensions { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/StrictRedirectUriValidatorAppAuthValidation.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/StrictRedirectUriValidatorAppAuthValidation.cs index 40bdb349f..b8d881641 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/StrictRedirectUriValidatorAppAuthValidation.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/StrictRedirectUriValidatorAppAuthValidation.cs @@ -2,14 +2,14 @@ // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. using AwesomeAssertions; -using IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Common; using Open.IdentityServer.Models; using Open.IdentityServer.Validation; using System.Collections.Generic; using System.Threading.Tasks; using Xunit; -namespace IdentityServer.UnitTests.Validation; +namespace Open.IdentityServer.UnitTests.Validation; public class StrictRedirectUriValidatorAppAuthValidation { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_ClientCredentials_Invalid.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_ClientCredentials_Invalid.cs index 266e070a8..cd004aebe 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_ClientCredentials_Invalid.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_ClientCredentials_Invalid.cs @@ -5,12 +5,12 @@ using System.Collections.Specialized; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Validation.Setup; +using Open.IdentityServer.UnitTests.Validation.Setup; using Open.IdentityServer; using Open.IdentityServer.Stores; using Xunit; -namespace IdentityServer.UnitTests.Validation.TokenRequest_Validation; +namespace Open.IdentityServer.UnitTests.Validation.TokenRequest_Validation; public class TokenRequestValidation_ClientCredentials_Invalid { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_Code_Invalid.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_Code_Invalid.cs index 4e9599b64..0ab129735 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_Code_Invalid.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_Code_Invalid.cs @@ -8,15 +8,15 @@ using System.Security.Claims; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; -using IdentityServer.UnitTests.Validation.Setup; +using Open.IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Validation.Setup; using Open.IdentityServer; using Open.IdentityServer.Configuration; using Open.IdentityServer.Models; using Open.IdentityServer.Stores; using Xunit; -namespace IdentityServer.UnitTests.Validation.TokenRequest_Validation; +namespace Open.IdentityServer.UnitTests.Validation.TokenRequest_Validation; public class TokenRequestValidation_Code_Invalid { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_DeviceCode_Invalid.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_DeviceCode_Invalid.cs index 2755c5f59..17cf18d94 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_DeviceCode_Invalid.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_DeviceCode_Invalid.cs @@ -6,15 +6,15 @@ using System.Collections.Specialized; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; -using IdentityServer.UnitTests.Validation.Setup; +using Open.IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Validation.Setup; using Open.IdentityServer; using Open.IdentityServer.Configuration; using Open.IdentityServer.Models; using Open.IdentityServer.Stores; using Xunit; -namespace IdentityServer.UnitTests.Validation.TokenRequest_Validation; +namespace Open.IdentityServer.UnitTests.Validation.TokenRequest_Validation; public class TokenRequestValidation_DeviceCode_Invalid { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_ExtensionGrants_Invalid.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_ExtensionGrants_Invalid.cs index d9af566bf..c82a55f29 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_ExtensionGrants_Invalid.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_ExtensionGrants_Invalid.cs @@ -5,12 +5,12 @@ using System.Collections.Specialized; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Validation.Setup; +using Open.IdentityServer.UnitTests.Validation.Setup; using Open.IdentityServer; using Open.IdentityServer.Stores; using Xunit; -namespace IdentityServer.UnitTests.Validation.TokenRequest_Validation; +namespace Open.IdentityServer.UnitTests.Validation.TokenRequest_Validation; public class TokenRequestValidation_ExtensionGrants_Invalid { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_General_Invalid.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_General_Invalid.cs index c77fcbc64..390e2329f 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_General_Invalid.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_General_Invalid.cs @@ -7,13 +7,13 @@ using System.Security.Claims; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Validation.Setup; +using Open.IdentityServer.UnitTests.Validation.Setup; using Open.IdentityServer; using Open.IdentityServer.Models; using Open.IdentityServer.Stores; using Xunit; -namespace IdentityServer.UnitTests.Validation.TokenRequest_Validation; +namespace Open.IdentityServer.UnitTests.Validation.TokenRequest_Validation; public class TokenRequestValidation_General_Invalid { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_PKCE.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_PKCE.cs index b64bb5e1b..ab5780bad 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_PKCE.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_PKCE.cs @@ -8,8 +8,8 @@ using System.Text; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; -using IdentityServer.UnitTests.Validation.Setup; +using Open.IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Validation.Setup; using Open.IdentityServer; using Open.IdentityServer.Configuration; using Open.IdentityServer.Models; @@ -17,7 +17,7 @@ using Open.IdentityServer.Utility; using Xunit; -namespace IdentityServer.UnitTests.Validation.TokenRequest_Validation; +namespace Open.IdentityServer.UnitTests.Validation.TokenRequest_Validation; public class TokenRequestValidation_PKCE { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_RefreshToken_Invalid.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_RefreshToken_Invalid.cs index 4c5ed54cb..3f398612c 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_RefreshToken_Invalid.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_RefreshToken_Invalid.cs @@ -8,15 +8,15 @@ using System.Security.Claims; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; -using IdentityServer.UnitTests.Validation.Setup; +using Open.IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Validation.Setup; using Open.IdentityServer; using Open.IdentityServer.Configuration; using Open.IdentityServer.Models; using Open.IdentityServer.Stores; using Xunit; -namespace IdentityServer.UnitTests.Validation.TokenRequest_Validation; +namespace Open.IdentityServer.UnitTests.Validation.TokenRequest_Validation; public class TokenRequestValidation_RefreshToken_Invalid { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_ResourceIndicators.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_ResourceIndicators.cs index abeef73e9..334b159ac 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_ResourceIndicators.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_ResourceIndicators.cs @@ -6,13 +6,13 @@ using System.Collections.Specialized; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Validation.Setup; +using Open.IdentityServer.UnitTests.Validation.Setup; using Open.IdentityServer; using Open.IdentityServer.Models; using Open.IdentityServer.Stores; using Xunit; -namespace IdentityServer.UnitTests.Validation.TokenRequest_Validation; +namespace Open.IdentityServer.UnitTests.Validation.TokenRequest_Validation; public class TokenRequestValidation_ResourceIndicators { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_ResourceOwner_Invalid.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_ResourceOwner_Invalid.cs index 91783e2dc..8058ee593 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_ResourceOwner_Invalid.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_ResourceOwner_Invalid.cs @@ -5,15 +5,15 @@ using System.Collections.Specialized; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Common; -using IdentityServer.UnitTests.Validation.Setup; +using Open.IdentityServer.UnitTests.Common; +using Open.IdentityServer.UnitTests.Validation.Setup; using Open.IdentityServer; using Open.IdentityServer.Models; using Open.IdentityServer.Stores; using Open.IdentityServer.Validation; using Xunit; -namespace IdentityServer.UnitTests.Validation.TokenRequest_Validation; +namespace Open.IdentityServer.UnitTests.Validation.TokenRequest_Validation; public class TokenRequestValidation_ResourceOwner_Invalid { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_Valid.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_Valid.cs index d8abc17df..adf98e034 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_Valid.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/TokenRequest Validation/TokenRequestValidation_Valid.cs @@ -8,13 +8,13 @@ using System.Collections.Specialized; using System.Threading.Tasks; using AwesomeAssertions; -using IdentityServer.UnitTests.Validation.Setup; +using Open.IdentityServer.UnitTests.Validation.Setup; using Open.IdentityServer; using Open.IdentityServer.Models; using Open.IdentityServer.Stores; using Xunit; -namespace IdentityServer.UnitTests.Validation.TokenRequest_Validation; +namespace Open.IdentityServer.UnitTests.Validation.TokenRequest_Validation; public class TokenRequestValidation_Valid { diff --git a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/UserInfoRequestValidation.cs b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/UserInfoRequestValidation.cs index 654b58578..b994408b7 100644 --- a/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/UserInfoRequestValidation.cs +++ b/src/Open.IdentityServer/test/Open.IdentityServer.UnitTests/Validation/UserInfoRequestValidation.cs @@ -1,24 +1,28 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Modified by Rock Solid Knowledge Ltd. Copyright in modifications 2026, Rock Solid Knowledge Ltd. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. using AwesomeAssertions; -using IdentityServer.UnitTests.Validation.Setup; +using Open.IdentityServer.UnitTests.Validation.Setup; using Open.IdentityServer.Stores; using Open.IdentityServer.Validation; using System.Collections.Generic; using System.Security.Claims; using System.Threading.Tasks; -using IdentityServer.UnitTests.Common; -using Open.IdentityServer; +using Moq; +using Open.IdentityServer.Services; +using Open.IdentityServer.UnitTests.Common; using Xunit; -namespace IdentityServer.UnitTests.Validation; +namespace Open.IdentityServer.UnitTests.Validation; public class UserInfoRequestValidation { private const string Category = "UserInfo Request Validation Tests"; private IClientStore _clients = new InMemoryClientStore(TestClients.Get()); + private Mock _telemetry = new(); + private Mock _trace = new(); [Fact] [Trait("Category", Category)] @@ -34,7 +38,8 @@ public async Task token_without_sub_should_fail() var validator = new UserInfoRequestValidator( new TestTokenValidator(tokenResult), new TestProfileService(shouldBeActive: true), - TestLogger.Create()); + _telemetry.Object, + TestLogger.Create()); var result = await validator.ValidateRequestAsync("token"); @@ -59,6 +64,7 @@ public async Task active_user_should_succeed() var validator = new UserInfoRequestValidator( new TestTokenValidator(tokenResult), new TestProfileService(shouldBeActive: true), + _telemetry.Object, TestLogger.Create()); var result = await validator.ValidateRequestAsync("token"); @@ -83,6 +89,7 @@ public async Task inactive_user_should_fail() var validator = new UserInfoRequestValidator( new TestTokenValidator(tokenResult), new TestProfileService(shouldBeActive: false), + _telemetry.Object, TestLogger.Create()); var result = await validator.ValidateRequestAsync("token"); @@ -90,4 +97,34 @@ public async Task inactive_user_should_fail() result.IsError.Should().BeTrue(); result.Error.Should().Be(OidcConstants.ProtectedResourceErrors.InvalidToken); } + + [Fact] + [Trait("Category", Category)] + public async Task validateRequest_should_initiate_telemetry_trace() + { + _telemetry.Setup(t => t.Trace(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_trace.Object); + + var tokenResult = new TokenValidationResult + { + IsError = false, + Client = await _clients.FindEnabledClientByIdAsync("codeclient"), + Claims = new List + { + new Claim("sub", "123") + }, + }; + + var validator = new UserInfoRequestValidator( + new TestTokenValidator(tokenResult), + new TestProfileService(shouldBeActive: true), + _telemetry.Object, + TestLogger.Create()); + + await validator.ValidateRequestAsync("token"); + + _telemetry.Verify(t => t.Trace( + TelemetryConstants.TraceCategories.Validation, validator, "ValidateRequestAsync")); + _trace.Verify(t => t.Dispose(), Times.Once); + } } \ No newline at end of file diff --git a/src/Storage/src/Services/ITelemetryService.cs b/src/Storage/src/Services/ITelemetryService.cs new file mode 100644 index 000000000..b10ab2f4f --- /dev/null +++ b/src/Storage/src/Services/ITelemetryService.cs @@ -0,0 +1,149 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace Open.IdentityServer.Services; + +/// +/// Defines methods for recording telemetry data about identity server operations and active requests. +/// +public interface ITelemetryService +{ + /// + /// Records a metric indicating that an operation encountered an unexpected error. + /// + /// The type of the error. + /// Error details will be exposed through the logs + void CountInternalError(string error); + + /// + /// Begins tracking an active request for the specified endpoint and path. + /// Dispose the returned when the request completes to stop tracking. + /// + /// The logical name of the endpoint handling the request. + /// The request path. + /// An that, when disposed, marks the request as completed. + IDisposable BeginActiveRequest(string endpoint, string path); + + /// + /// Records a metric for an API secret validation attempt. + /// + /// The client id + /// The authMethod, on success + /// The error, on failure + void CountApiSecretValidation(string client, string authMethod = null, string error = null); + + /// + /// Records a metric for a backchannel authentication request (CIBA). + /// + /// The client id + /// The error, on failure + void CountBackchannelAuthentication(string client, string error = null); + + /// + /// Records a metric for a client configuration validation attempt. + /// + /// The client id + /// The error, on failure + void CountClientConfigValidation(string client, string error = null); + + /// + /// Records a metric for an API secret validation attempt. + /// + /// The client id + /// The authMethod, on success + /// The error, on failure + void CountClientSecretValidation(string client, string authMethod = null, string error = null); + + /// + /// Records a metric for a device authentication request. + /// + /// The client id + /// The error, on failure + void CountDeviceAuthentication(string client, string error = null); + + /// + /// Records a metric for a token introspection request. + /// + /// The caller + /// Is the token active, only set on success. + /// The error, on failure + void CountTokenIntrospection(string caller, bool? active = null, string error = null); + + /// + /// Records a metric for a pushed authorization request (PAR). + /// + /// The client id + /// The error, on failure + void CountPushedAuthorizationRequest(string client, string error = null); + + /// + /// Records a metric for a resource owner password authentication attempt. + /// + /// The client id + /// The error, on failure + void CountResourceOwnerAuthentication(string client, string error = null); + + /// + /// Records a metric for a token revocation request. + /// + /// The client id + /// The error, on failure + void CountTokenRevocation(string client, string error = null); + + /// + /// Records a metric for a token that was issued. + /// + /// The client id + /// The grant type + /// The error, on failure + void CountTokenIssued(string client, string grantType, string error = null); + + /// + /// Begins a trace activity for the specified category and activity name. + /// + /// The trace category + /// The name of the current operation + /// + ITrace Trace(string category, string activityName); + + /// + /// Begins a trace activity for the specified category. + /// Activity name will be a combination of the calling class name and method name. + /// + /// The trace category + /// The object initiating the trace + /// The name of the method initiating the trace + /// + ITrace Trace(string category, object caller, [CallerMemberName] string callingMethod = null); +} + +/// +/// Abstraction for a distributed trace +/// +public interface ITrace : IDisposable +{ + /// + /// Update the trace to have additional data + /// + /// The data key + /// The data value + ITrace AddTag(string key, string value); + + /// + /// Update the trace to have additional data + /// + /// The data key + /// The data value + ITrace AddTag(string key, object value); + + + /// + /// Update the trace to have additional details of a thrown exception. + /// + /// The exception + ITrace AddException(Exception exception); +} \ No newline at end of file diff --git a/src/Storage/src/TelemetryConstants.cs b/src/Storage/src/TelemetryConstants.cs new file mode 100644 index 000000000..5e64af765 --- /dev/null +++ b/src/Storage/src/TelemetryConstants.cs @@ -0,0 +1,65 @@ +// Copyright (c) 2026, Rock Solid Knowledge Ltd +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +namespace Open.IdentityServer; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +public static class TelemetryConstants +{ + public static class MetricsConstants + { + public const string MeterName = "Open.IdentityServer"; + public const string OperationCounterName = "tokenservice.operation"; + public const string ActiveRequestsCounterName = "tokenservice.active_requests"; + + public const string ApiSecretValidationCounterName = "tokenservice.api.secret_validation"; + public const string ClientConfigValidationCounterName = "tokenservice.client.config_validation"; + public const string ClientSecretValidationCounterName = "tokenservice.client.secret_validation"; + public const string DeviceAuthenticationCounterName = "tokenservice.device_authentication"; + public const string IntrospectionCounterName = "tokenservice.introspection"; + public const string ResourceOwnerAuthenticationCounterName = "tokenservice.resourceowner_authentication"; + public const string TokenIssuedCounterName = "tokenservice.token_issued"; + public const string RevocationCounterName = "tokenservice.revocation"; + + // Not yet implemented, on roadmap, included + public const string PushedAuthorizationRequestCounterName = "tokenservice.pushed_authorization_request"; + public const string BackChannelAuthenticationCounterName = "tokenservice.backchannel_authentication"; + + // Metrics for component packages + public const string DynamicIdentityProviderCounterName = "tokenservice.dynamic_identityprovider.validation"; + public const string SamlSsoCounterName = "tokenservice.saml.sso"; + public const string SamlSloCounterName = "tokenservice.saml.slo"; + } + + public static class TagConstants + { + public const string Error = "error"; + public const string Result = "result"; + public const string Client = "client"; + + public const string Success = "success"; + public const string InternalError = "internal_error"; + public const string Endpoint = "endpoint"; + public const string Path = "path"; + public const string Api = "api"; + public const string AuthMethod = "auth_method"; + public const string Scheme = "scheme"; + public const string Caller = "caller"; + public const string Active = "active"; + public const string GrantType = "grant_type"; + public const string Scope = "scope"; + public const string Subject = "subject"; + public const string Session = "session"; + } + + public static class TraceCategories + { + public const string Basic = "Open.IdentityServer"; + public const string Cache = "Open.IdentityServer.Cache"; + public const string Services = "Open.IdentityServer.Services"; + public const string Stores = "Open.IdentityServer.Stores"; + public const string Validation = "Open.IdentityServer.Validation"; + } +} + +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member \ No newline at end of file