Introducing the next era of Duende IdentityServer.

Read our CEO’s announcement

Monitoring Duende IdentityServer License Usage with ASP.NET Core Health Checks

Maarten Balliauw
Two blue circles

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 to AddLicenseSummary() when registering IdentityServer:

Csharp

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:

Csharp

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.

Csharp

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.

Csharp

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.

Csharp

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:

Csharp

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:

Csharp

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:

Csharp

return Task.FromResult(
    HealthCheckResult.Healthy(
        description: "Duende IdentityServer license is valid.",
        data: healthCheckData));

Here is the complete implementation of the custom health check:

Csharp

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:

Csharp

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:

Csharp

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:

Csharp

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:

Csharp

{
  "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.

Configuring Health Check Failure Status

Health checks are a great way to monitor the health of your application and its dependencies. However, they should be
used with care, especially when used in conjunction with load balancers or in environments with liveness checks like Kubernetes.

In such environments, when a health check reports failure on an instance of your application, the load balancer will mark
the instance as unhealthy. This will cause the it to be removed from the load balancer pool until it reports being healthy again.

With the health check described in this post, you will want to decide whether an expired license really means your IdentityServer
is unhealthy: an expired license is not necessarily a technical problem, and your application will keep running.
However when reporting "unhealthy", your load balancer may stop sending traffic to it.

Perhaps it is better to report a "degraded" status instead of "unhealthy",
and let the load balancer continue to send traffic to the instance.

The health check described in this post is built so that it supports configuring what
status to report on failure. Instead of always returning HealthStatus.Unhealthy,
it uses the context.Registration.FailureStatus property to determine what status to return.

This FailureStatus can be configured when registering the health check in your Program.cs
file:

Csharp

builder.Services.AddHealthChecks()
    .AddCheck<DuendeIdentityServerLicenseHealthCheck>("identityserver", HealthStatus.Degraded);

This will make sure a failed license check does not report "unhealthy",
and instead returns "degraded".

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!