From duende-skills
Orchestrate Duende IdentityServer in .NET Aspire AppHost — dependency graphs, authority URL wiring, health checks, and multi-instance.
How this skill is triggered — by the user, by Claude, or both
Slash command
/duende-skills:identityserver-aspireThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Use this skill when:
Use this skill when:
WithReference() + WaitFor().IConfiguration/IOptions<T>, never Aspire service discovery.WaitFor() requires the target to expose a healthy health check endpoint. IdentityServer must be configured with health checks for startup ordering to work.identityserver-hosting-setup — IdentityServer DI and middleware pipelineidentityserver-deployment — production deployment, data protection, health check implementationsidentityserver-data-storage — EF Core stores for configuration and operational dataDocs: https://docs.duendesoftware.com/identityserver/aspire
IdentityServer is added to an Aspire AppHost like any ASP.NET Core project. The key addition is wiring its database dependency so the database is ready before IdentityServer starts.
var builder = DistributedApplication.CreateBuilder(args);
var sqlServer = builder.AddSqlServer("sql");
var identityDb = sqlServer.AddDatabase("identitydb");
var identityServer = builder.AddProject<Projects.IdentityServer>("identity-server")
.WithReference(identityDb)
.WaitFor(sqlServer);
builder.Build().Run();
WaitFor(sqlServer) ensures the database is accepting connections before IdentityServer
starts. This matters because IdentityServer connects to EF Core stores on startup for
configuration and operational data.
Important: The IdentityServer project itself is a standard ASP.NET Core application. See
identityserver-hosting-setupfor DI registration and middleware pipeline setup. This skill focuses only on how the AppHost orchestrates it.
This is the most critical pattern. Clients and APIs must depend on IdentityServer because:
.well-known/openid-configuration) at
startup to configure OIDC flowsWithout explicit dependency ordering, services start in parallel and fail with cryptic "unable to obtain configuration" errors.
var builder = DistributedApplication.CreateBuilder(args);
var sqlServer = builder.AddSqlServer("sql");
var identityDb = sqlServer.AddDatabase("identitydb");
var identityServer = builder.AddProject<Projects.IdentityServer>("identity-server")
.WithReference(identityDb)
.WaitFor(sqlServer);
var api = builder.AddProject<Projects.WeatherApi>("weather-api")
.WithReference(identityServer)
.WaitFor(identityServer);
var webApp = builder.AddProject<Projects.WebApp>("web-app")
.WithReference(identityServer)
.WaitFor(identityServer)
.WithReference(api);
builder.Build().Run();
| Call | Effect |
|---|---|
.WithReference(identityServer) | Makes the IdentityServer endpoint URL available to the dependent service via service discovery |
.WaitFor(identityServer) | Holds the dependent service from starting until IdentityServer's health check returns healthy |
Both are needed. WithReference alone provides the URL but doesn't prevent premature
startup. WaitFor alone doesn't expose the endpoint URL.
sqlServer ─► identity-server ─► weather-api
─► web-app ──► weather-api
Important: Without
WaitFor(identityServer), the API and web app may start before IdentityServer is ready, causingHttpRequestExceptionwhen fetching the discovery document or JWKS. This leads toInvalidOperationException: IDX20803: Unable to obtain configuration from 'https://.../.well-known/openid-configuration'errors at startup.
WithReference(identityServer) makes the endpoint available via Aspire service discovery,
but client applications need explicit configuration for the OIDC authority URL, client ID,
and scopes. Use WithEnvironment to pass these as standard configuration values.
var webApp = builder.AddProject<Projects.WebApp>("web-app")
.WithReference(identityServer)
.WaitFor(identityServer)
.WithEnvironment("Authentication__Authority", identityServer.GetEndpoint("https"))
.WithEnvironment("Authentication__ClientId", "web-app")
.WithEnvironment("Authentication__Scopes__0", "openid")
.WithEnvironment("Authentication__Scopes__1", "profile")
.WithEnvironment("Authentication__Scopes__2", "weather.read");
var api = builder.AddProject<Projects.WeatherApi>("weather-api")
.WithReference(identityServer)
.WaitFor(identityServer)
.WithEnvironment("Authentication__Authority", identityServer.GetEndpoint("https"));
By default, IdentityServer infers the issuer URI from incoming requests, which works correctly within Aspire's network. Only override if the internal URL differs from what clients see:
var identityServer = builder.AddProject<Projects.IdentityServer>("identity-server")
.WithReference(identityDb)
.WaitFor(sqlServer)
.WithEnvironment("IdentityServer__IssuerUri", identityServer.GetEndpoint("https"));
Important: Do NOT set
IssuerUriunless the internal Aspire URL differs from what clients see. Mismatched issuer URIs cause token validation failures — theissclaim in tokens won't match the expected authority.
See aspire-configuration for the general pattern of reading these values via IOptions<T>
in the app project.
When generating app code: The environment variables above map to standard
IConfigurationkeys (Authentication:Authority,Authentication:ClientId,Authentication:Scopes:0, etc.). When scaffolding the web app, configureAddOpenIdConnectto readAuthorityandClientIdfrombuilder.Configuration["Authentication:Authority"]andbuilder.Configuration["Authentication:ClientId"]. Bind scopes from theAuthentication:Scopesconfiguration section. For the API, configureAddJwtBearerwithoptions.Authorityfrombuilder.Configuration["Authentication:Authority"]. Seeaspnetcore-authenticationfor full OIDC and JWT Bearer middleware setup. Seeaspire-configurationfor the generalIOptions<T>binding pattern.
IdentityServer typically needs its own database for configuration and operational stores. Other services in the solution use separate databases for application data.
var sqlServer = builder.AddSqlServer("sql");
var identityDb = sqlServer.AddDatabase("identitydb");
var appDb = sqlServer.AddDatabase("appdb");
var identityServer = builder.AddProject<Projects.IdentityServer>("identity-server")
.WithReference(identityDb)
.WaitFor(sqlServer);
var api = builder.AddProject<Projects.WeatherApi>("weather-api")
.WithReference(appDb)
.WaitFor(sqlServer)
.WithReference(identityServer)
.WaitFor(identityServer);
WithReference(identityDb) sets ConnectionStrings__identitydb automatically. The
IdentityServer project's EF stores must use this connection string name:
// In IdentityServer's Program.cs
builder.Services.AddIdentityServer()
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = b =>
b.UseSqlServer(builder.Configuration.GetConnectionString("identitydb"));
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = b =>
b.UseSqlServer(builder.Configuration.GetConnectionString("identitydb"));
});
Database.MigrateAsync() in Program.cs. Simple
and suitable for development.var migrations = builder.AddProject<Projects.MigrationRunner>("migrations")
.WithReference(identityDb)
.WaitFor(sqlServer);
var identityServer = builder.AddProject<Projects.IdentityServer>("identity-server")
.WithReference(identityDb)
.WaitFor(migrations); // Wait for migrations to complete
See identityserver-data-storage for EF Core store configuration details and migration
patterns.
Aspire's WaitFor() polls the target's /health endpoint. If IdentityServer doesn't
expose a health check, WaitFor() has no readiness signal and dependent services may
start too early or the AppHost may time out waiting.
// In IdentityServer's Program.cs
builder.Services.AddHealthChecks();
// After building the app
app.MapHealthChecks("/health");
For production-grade startup ordering, add checks that validate IdentityServer can actually serve discovery documents and signing keys:
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy(), tags: ["live"])
.AddCheck<DiscoveryDocumentHealthCheck>("discovery", tags: ["ready"])
.AddCheck<DiscoveryKeysHealthCheck>("jwks", tags: ["ready"]);
app.MapHealthChecks("/health");
app.MapHealthChecks("/alive", new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("live")
});
app.MapHealthChecks("/ready", new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("ready")
});
The DiscoveryDocumentHealthCheck and DiscoveryKeysHealthCheck verify that IdentityServer
can serve its discovery document and signing keys. These checks catch configuration errors
(missing signing credentials, database connection failures) before dependent services try
to connect.
Important: The
DiscoveryDocumentHealthCheckandDiscoveryKeysHealthCheckimplementations are covered in theidentityserver-deploymentskill. Use them to ensure IdentityServer is fully operational before dependent services start.
If using AddServiceDefaults() from the Aspire service defaults project, the /health
and /alive endpoints are already mapped. You still need to register the
IdentityServer-specific health checks in the DI container.
IdentityServer emits OpenTelemetry traces and metrics under specific source names. To see them in the Aspire dashboard, add these sources in the shared service defaults project.
Add IdentityServer activity sources to ConfigureOpenTelemetry in the service defaults
Extensions.cs:
tracing
.AddSource(builder.Environment.ApplicationName)
// Duende IdentityServer trace sources
.AddSource("Duende.IdentityServer")
.AddSource("Duende.IdentityServer.Cache")
.AddSource("Duende.IdentityServer.Services")
.AddSource("Duende.IdentityServer.Stores")
.AddSource("Duende.IdentityServer.Validation")
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation();
Add the IdentityServer meter:
metrics
.AddMeter("Duende.IdentityServer")
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation();
Important: Use string literals (not
IdentityServerConstants.Tracing.*orTelemetry.ServiceName) in service defaults to avoid adding a Duende.IdentityServer package reference to the shared project. Only the IdentityServer project itself should reference the Duende package.
See aspire-service-defaults for the full service defaults setup pattern and
identityserver-deployment for detailed telemetry guidance.
Aspire supports running multiple instances of a project with WithReplicas:
var identityServer = builder.AddProject<Projects.IdentityServer>("identity-server")
.WithReference(identityDb)
.WaitFor(sqlServer)
.WithReplicas(3);
Running multiple IdentityServer instances requires shared state across all replicas:
ISigningKeyStore (EF operational store or custom implementation)See identityserver-deployment for data protection and operational store configuration.
See identityserver-data-storage for EF Core store setup.
Important: Do NOT use
.WithReplicas(n)without first configuring shared state. Multiple instances with file-based signing keys or in-memory stores will produce token validation failures, lost sessions, and authentication cookie errors.
When integration testing an Aspire solution that includes IdentityServer, the key challenge is that the authority URL uses a dynamic port assigned at runtime. Test clients must discover the URL from the test fixture.
public sealed class IdentityAspireFixture : IAsyncLifetime
{
private DistributedApplication? _app;
public async Task InitializeAsync()
{
var builder = await DistributedApplicationTestingBuilder
.CreateAsync<Projects.MyApp_AppHost>();
_app = await builder.BuildAsync();
await _app.StartAsync();
// Wait for IdentityServer to be healthy before running tests
await _app.ResourceNotifications
.WaitForResourceHealthyAsync("identity-server");
}
public Uri GetAuthorityUrl() =>
_app!.GetEndpoint("identity-server", "https");
public HttpClient CreateApiClient() =>
_app!.CreateHttpClient("weather-api");
public async Task DisposeAsync()
{
if (_app is not null)
{
await _app.StopAsync();
await _app.DisposeAsync();
}
}
}
The important details:
WaitForResourceHealthyAsync("identity-server") ensures IdentityServer is fully
ready before any test runs. The resource name matches the name in the AppHost.GetEndpoint("identity-server", "https") returns the dynamic https://localhost:{port}
URL. Use this as the authority when configuring test HttpClient instances.CreateHttpClient("weather-api") creates a client pre-configured with the API's
dynamic base address.Do
WithReference() + WaitFor() for every service that depends on IdentityServerDon't
WaitFor(identityServer) — causes discovery failuresWithReplicas() without configuring shared state (signing keys, data protection, operational store)IssuerUri unless the internal and external URLs actually differnpx claudepluginhub duendesoftware/duende-skills --plugin duende-skillsProvides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.