Monitoring Duende IdentityServer License Usage with ASP.NET Core Health Checks
Health checks are vital for maintaining the reliability and performance of modern applications. They provide a systematic way to monitor the health and status of your application and its dependencies. ASP.NET Core offers built-in support for health checks, and with the help of third-party packages, you can easily integrate these checks with popular monitoring systems like Prometheus, Grafana, and Azure Application Insights.
With health checks, you can monitor various aspects of your application, including dependencies (e.g., databases, external services), specific metrics (e.g., response times, error rates), and compliance with licensing or other operational requirements. For IdentityServer, you may want to monitor the health of the discovery endpoint, or monitor license compliance to ensure your application remains within the terms of the license agreement.
In this blog post, we will see how to implement a custom ASP.NET Core health check that reports on IdentityServer license status and usage.
Creating a Custom ASP.NET Core Health Check
Duende IdentityServer is a popular framework for implementing OpenID Connect and OAuth 2.0 in ASP.NET Core applications. It requires a valid license to operate in production environments. Monitoring the status and usage of your Duende IdentityServer license is important to ensure that your application remains compliant.
There are two classes available in the ASP.NET Core service provider that are helpful for getting information about the current license and its usage in your IdentityServer implementation:
IdentityServerLicense
- A class that provides information about your license, such as the company name it is registered to, which IdentityServer edition it supports, its expiry date, and more.LicenseUsageSummary
- A class that provides information about license usage, for example, the number of registered clients, the number of issuers, features being used, and more.
Note: To make
LicenseUsageSummary
available in your application, you’ll need to ensure it is added at startup. You can do this with a call toAddLicenseSummary()
when registering IdentityServer:builder.Services.AddIdentityServer() .AddLicenseSummary();
By implementing a custom health check for Duende IdentityServer, you can integrate the license status and usage information with your existing monitoring and alerting systems.
To create a custom health check that monitors the Duende IdentityServer license, you need to implement the IHealthCheck
interface. Here’s the outline for a custom class DuendeIdentityServerLicenseHealthCheck
that uses the IdentityServerLicense
and LicenseUsageSummary
from ASP.NET Core’s service provider:
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace AcmeCorp.IdentityServer;
public class DuendeIdentityServerLicenseHealthCheck(
IHostEnvironment environment,
LicenseUsageSummary? licenseUsageSummary,
IdentityServerLicense? license = null)
: IHealthCheck
{
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
// ...
}
}
The CheckHealthAsync
method is where the magic of a health check happens. Let’s break down the implementation step by step.
At a minimum, a health check should provide ASP.NET Core with a general status, such as “healthy”, “degraded”, or “unhealthy”. You can also provide a dictionary with additional information, such as the license mode, license expiry, etc.
var healthCheckData = new Dictionary<string, object>();
healthCheckData["mode"] = _license == null
? "trial"
: _license.Expiration < DateTime.UtcNow
? "expired"
: "active";
A first entry is added to the custom health check data dictionary, adding information about whether the license is trial, expired, or active.
If the license is available, you can add some its details to the health check data as well.
if (_license != null)
{
if (_license.Expiration != null)
{
healthCheckData["expiration"] = _license.Expiration;
}
}
Important: health check endpoints are public by default, so be careful to not disclose information such as issuer URLs, client IDs, and the license edition/serial through health checks. Keep the information disclosed here to a minimum, or consider reporting them as metrics using OpenTelemetry instead.
Similarly, if the license usage summary is available, add its details to the health check data.
if (_licenseUsageSummary != null)
{
healthCheckData["clients_count"] = _licenseUsageSummary.ClientsUsed.Count;
healthCheckData["issuers_count"] = _licenseUsageSummary.IssuersUsed.Count;
}
Next, you can return the overall status of the health check. When the current environment is production, a license is required. You can check if the license is available, or whether it is expired, and based on that provide a return value for the custom health check:
if (environment.IsProduction())
{
if (license == null)
{
return Task.FromResult(
new HealthCheckResult(
status: context.Registration.FailureStatus,
description: "Missing Duende IdentityServer license.",
data: healthCheckData));
}
if (license != null && license.Expiration < DateTime.Now)
{
return Task.FromResult(
new HealthCheckResult(
status: context.Registration.FailureStatus,
description: "Duende IdentityServer license has expired.",
data: healthCheckData));
}
}
When not running in production, for example, for a dev/test/QA environment, no license is required. You can reflect this in the custom health check:
if (!environment.IsProduction())
{
return Task.FromResult(
HealthCheckResult.Healthy(
description: "Duende IdentityServer license is not required in non-production environments.",
data: healthCheckData));
}
When all seems OK, you can return a healthy status:
return Task.FromResult(
HealthCheckResult.Healthy(
description: "Duende IdentityServer license is valid.",
data: healthCheckData));
Here is the complete implementation of the custom health check:
using Duende.IdentityServer;
using Duende.IdentityServer.Licensing;
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace AcmeCorp.IdentityServer;
public class DuendeIdentityServerLicenseHealthCheck(
IHostEnvironment environment,
LicenseUsageSummary? licenseUsageSummary,
IdentityServerLicense? license = null)
: IHealthCheck
{
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
var healthCheckData = new Dictionary<string, object>();
healthCheckData["mode"] = license == null
? "trial"
: license.Expiration < DateTime.UtcNow
? "expired"
: "active";
if (license != null)
{
if (license.Expiration != null)
{
healthCheckData["expiration"] = license.Expiration;
}
}
if (licenseUsageSummary != null)
{
healthCheckData["clients_count"] = licenseUsageSummary.ClientsUsed.Count;
healthCheckData["issuers_count"] = licenseUsageSummary.IssuersUsed.Count;
}
if (environment.IsProduction())
{
if (license == null)
{
return Task.FromResult(
new HealthCheckResult(
status: context.Registration.FailureStatus,
description: "Missing Duende IdentityServer license.",
data: healthCheckData));
}
if (license != null && license.Expiration < DateTime.Now)
{
return Task.FromResult(
new HealthCheckResult(
status: context.Registration.FailureStatus,
description: "Duende IdentityServer license has expired.",
data: healthCheckData));
}
}
if (!environment.IsProduction())
{
return Task.FromResult(
HealthCheckResult.Healthy(
description: "Duende IdentityServer license is not required in non-production environments.",
data: healthCheckData));
}
return Task.FromResult(
HealthCheckResult.Healthy(
description: "Duende IdentityServer license is valid.",
data: healthCheckData));
}
}
Registering the Health Check
Before being able to retrieve the result of the custom health check just created, you will need to register it by adding it to the health checks builder in your Program.cs
file:
builder.Services.AddHealthChecks()
.AddCheck<DuendeIdentityServerLicenseHealthCheck>("identityserver");
Additionally, you need to add the health check to the ASP.NET Core request pipeline if you haven’t done this yet. This will expose health check information at the /health
endpoint of your IdentityServer host:
app.MapHealthChecks("/health");
Note that by default, the health check endpoint only displays overall health for all health checks combined. If you want detailed data exposed here, you can register a custom response writer that, for example, returns JSON. A sample is in the official documentation, or you can use the following implementation:
app.MapHealthChecks("health", new HealthCheckOptions
{
ResponseWriter = (context, healthReport) =>
{
context.Response.ContentType = "application/json; charset=utf-8";
var options = new JsonWriterOptions { Indented = true };
using var memoryStream = new MemoryStream();
using (var jsonWriter = new Utf8JsonWriter(memoryStream, options))
{
jsonWriter.WriteStartObject();
jsonWriter.WriteString("status", healthReport.Status.ToString());
jsonWriter.WriteStartObject("results");
foreach (var healthReportEntry in healthReport.Entries)
{
jsonWriter.WriteStartObject(healthReportEntry.Key);
jsonWriter.WriteString("status",
healthReportEntry.Value.Status.ToString());
jsonWriter.WriteString("description",
healthReportEntry.Value.Description);
jsonWriter.WriteStartObject("data");
foreach (var item in healthReportEntry.Value.Data)
{
jsonWriter.WritePropertyName(item.Key);
JsonSerializer.Serialize(jsonWriter, item.Value,
item.Value?.GetType() ?? typeof(object));
}
jsonWriter.WriteEndObject();
jsonWriter.WriteEndObject();
}
jsonWriter.WriteEndObject();
jsonWriter.WriteEndObject();
}
return context.Response.WriteAsync(
Encoding.UTF8.GetString(memoryStream.ToArray()));
}
});
When visiting the /health
endpoint, the response now will look like the following:
{
"status": "Healthy",
"results": {
"identityserver": {
"status": "Healthy",
"description": "Duende IdentityServer license is valid.",
"data": {
"mode": "active",
"expiration": "2026-03-03T00:00:00Z",
"clients_count": 2,
"issuers_count": 1
}
}
}
}
Once again, note the /health
endpoint is public and that providing full details about health checks including all their data may not always be desired. Consider providing detailed information only to authorized users by adding a second health check endpoint with the necessary authentication and authorization configured.
Publishing Health Check Data
To publish health check data to an external system, you can implement the IHealthCheckPublisher
interface in ASP.NET Core. This interface allows you to send health check results to external systems for monitoring and alerting purposes.
You most probably don’t want to roll your own health check publisher. A community project exists at AspNetCore.Diagnostics.HealthChecks provides publishers for various systems, including Application Insights, CloudWatch, Datadog, Prometheus Gateway, Seq, and many more. It also comes with many health checks for other service dependencies you may have.
Publishing health check data is preferred over exposing full health check information at the /health
endpoint (e.g. using JSON output), as the data is then only shared with a system that you control and ideally has access controls in place.
Other IdentityServer Health Checks
Next to monitoring license usage and validity, there are a number of other health checks you may want to implement and monitor.
To check the overall health of your Duende IdentityServer, you can make discovery requests. Successful discovery responses indicate that the IdentityServer host is running, able to receive requests and generate responses. In addition, a successful discovery response means your IdentityServer host can reliably communicate with the configuration store.
Another health check you can perform is requesting the public keys that IdentityServer uses to sign tokens (the JSON Web Key Set or JWKS). A successful health check validates that IdentityServer is able to communicate with an important dependency: the signing key store.
Our documentation includes examples of both of these health checks.
Summary
In this post, we explored how to use ASP.NET Core health checks to monitor the status and usage of your Duende IdentityServer license. We walked through creating a custom health check, registering it, and exposing it via an endpoint. We also looked at how to publish health check data to external systems and add a UI for easy monitoring. By setting up these health checks, you can keep an eye on your license compliance and ensure your application runs smoothly without any surprises.
Are you using health checks in your ASP.NET Core application? Are you monitoring service dependencies beyond just availability? Let us know in the comments!