{"id":18156,"plugin_id":"plugins_6a86acf7816881918552f3b43bc0db69","kind":"skill","collection_source":null,"comparison_source":null,"observed_at":"2026-09-30T23:14:40.988Z","digest":"62831029eec22e244e26e0c8b7b0fecef60657f98da807db855202db7cf4e418","against":null,"payload":{"description":"Orchestrate Duende IdentityServer in .NET Aspire AppHost — dependency graphs, authority URL wiring, health checks, and multi-instance.","included_files":[],"name":"identityserver-aspire","skill_md_contents":"---\nname: identityserver-aspire\ndescription: Orchestrate Duende IdentityServer in .NET Aspire AppHost — dependency graphs, authority URL wiring, health checks, and multi-instance.\ninvocable: false\n---\n\n# Orchestrating IdentityServer with .NET Aspire\n\n## When to Use This Skill\n\nUse this skill when:\n- Adding Duende IdentityServer to an Aspire-orchestrated solution\n- Configuring service dependencies so clients and APIs wait for IdentityServer\n- Passing IdentityServer's authority URL to dependent services via Aspire\n- Wiring database resources for IdentityServer configuration and operational stores\n- Ensuring IdentityServer exposes health checks for Aspire startup ordering\n- Adding IdentityServer telemetry sources to Aspire service defaults\n- Running multiple IdentityServer replicas in Aspire\n- Integration testing an Aspire solution that includes IdentityServer\n\n## Core Principles\n\n1. **IdentityServer is a startup dependency** — Every client app and API depends on IdentityServer being available for discovery, token validation, and OIDC flows. Model this with `WithReference()` + `WaitFor()`.\n2. **Explicit configuration over service discovery** — Pass the authority URL, client IDs, and scopes as explicit environment variables. App code reads `IConfiguration`/`IOptions<T>`, never Aspire service discovery.\n3. **Health checks enable startup ordering** — Aspire's `WaitFor()` requires the target to expose a healthy health check endpoint. IdentityServer must be configured with health checks for startup ordering to work.\n4. **Cross-reference, don't duplicate** — General Aspire and IdentityServer patterns are covered by existing skills. This skill covers only the unique orchestration intersection.\n\n## Related Skills\n\n- `identityserver-hosting-setup` — IdentityServer DI and middleware pipeline\n- `identityserver-deployment` — production deployment, data protection, health check implementations\n- `identityserver-data-storage` — EF Core stores for configuration and operational data\n\nDocs: https://docs.duendesoftware.com/identityserver/deployment/\n\n---\n\n## Pattern 1: AppHost Orchestration Basics\n\nIdentityServer is added to an Aspire AppHost like any ASP.NET Core project. The key\naddition is wiring its database dependency so the database is ready before IdentityServer\nstarts.\n\n```csharp\nvar builder = DistributedApplication.CreateBuilder(args);\n\nvar sqlServer = builder.AddSqlServer(\"sql\");\nvar identityDb = sqlServer.AddDatabase(\"identitydb\");\n\nvar identityServer = builder.AddProject<Projects.IdentityServer>(\"identity-server\")\n    .WithReference(identityDb)\n    .WaitFor(sqlServer);\n\nbuilder.Build().Run();\n```\n\n`WaitFor(sqlServer)` ensures the database is accepting connections before IdentityServer\nstarts. This matters because IdentityServer connects to EF Core stores on startup for\nconfiguration and operational data.\n\n> **Important:** The IdentityServer project itself is a standard ASP.NET Core application.\n> See `identityserver-hosting-setup` for DI registration and middleware pipeline setup.\n> This skill focuses only on how the AppHost orchestrates it.\n\n---\n\n## Pattern 2: Service Dependency Graph\n\nThis is the most critical pattern. Clients and APIs must depend on IdentityServer\nbecause:\n\n- **Clients** download the discovery document (`.well-known/openid-configuration`) at\n  startup to configure OIDC flows\n- **APIs** configured with JWT Bearer authentication download JWKS (signing keys) from\n  IdentityServer at startup to validate tokens\n- **OIDC login redirects** fail if IdentityServer isn't running when a user tries to\n  sign in\n\nWithout explicit dependency ordering, services start in parallel and fail with cryptic\n\"unable to obtain configuration\" errors.\n\n### Full dependency graph\n\n```csharp\nvar builder = DistributedApplication.CreateBuilder(args);\n\nvar sqlServer = builder.AddSqlServer(\"sql\");\nvar identityDb = sqlServer.AddDatabase(\"identitydb\");\n\nvar identityServer = builder.AddProject<Projects.IdentityServer>(\"identity-server\")\n    .WithReference(identityDb)\n    .WaitFor(sqlServer);\n\nvar api = builder.AddProject<Projects.WeatherApi>(\"weather-api\")\n    .WithReference(identityServer)\n    .WaitFor(identityServer);\n\nvar webApp = builder.AddProject<Projects.WebApp>(\"web-app\")\n    .WithReference(identityServer)\n    .WaitFor(identityServer)\n    .WithReference(api);\n\nbuilder.Build().Run();\n```\n\n### What each call does\n\n| Call | Effect |\n|------|--------|\n| `.WithReference(identityServer)` | Makes the IdentityServer endpoint URL available to the dependent service via service discovery |\n| `.WaitFor(identityServer)` | Holds the dependent service from starting until IdentityServer's health check returns healthy |\n\nBoth are needed. `WithReference` alone provides the URL but doesn't prevent premature\nstartup. `WaitFor` alone doesn't expose the endpoint URL.\n\n### Dependency flow\n\n```\nsqlServer ─► identity-server ─► weather-api\n                               ─► web-app ──► weather-api\n```\n\n> **Important:** Without `WaitFor(identityServer)`, the API and web app may start before\n> IdentityServer is ready, causing `HttpRequestException` when fetching the discovery\n> document or JWKS. This leads to `InvalidOperationException: IDX20803: Unable to obtain\n> configuration from 'https://.../.well-known/openid-configuration'` errors at startup.\n\n---\n\n## Pattern 3: Authority URL and OIDC Configuration\n\n`WithReference(identityServer)` makes the endpoint available via Aspire service discovery,\nbut client applications need explicit configuration for the OIDC authority URL, client ID,\nand scopes. Use `WithEnvironment` to pass these as standard configuration values.\n\n### Web application (OIDC client)\n\n```csharp\nvar webApp = builder.AddProject<Projects.WebApp>(\"web-app\")\n    .WithReference(identityServer)\n    .WaitFor(identityServer)\n    .WithEnvironment(\"Authentication__Authority\", identityServer.GetEndpoint(\"https\"))\n    .WithEnvironment(\"Authentication__ClientId\", \"web-app\")\n    .WithEnvironment(\"Authentication__Scopes__0\", \"openid\")\n    .WithEnvironment(\"Authentication__Scopes__1\", \"profile\")\n    .WithEnvironment(\"Authentication__Scopes__2\", \"weather.read\");\n```\n\n### API (JWT Bearer)\n\n```csharp\nvar api = builder.AddProject<Projects.WeatherApi>(\"weather-api\")\n    .WithReference(identityServer)\n    .WaitFor(identityServer)\n    .WithEnvironment(\"Authentication__Authority\", identityServer.GetEndpoint(\"https\"));\n```\n\n### Issuer URI consideration\n\nBy default, IdentityServer infers the issuer URI from incoming requests, which works\ncorrectly within Aspire's network. Only override if the internal URL differs from what\nclients see:\n\n```csharp\nvar identityServer = builder.AddProject<Projects.IdentityServer>(\"identity-server\")\n    .WithReference(identityDb)\n    .WaitFor(sqlServer)\n    .WithEnvironment(\"IdentityServer__IssuerUri\", identityServer.GetEndpoint(\"https\"));\n```\n\n> **Important:** Do NOT set `IssuerUri` unless the internal Aspire URL differs from what\n> clients see. Mismatched issuer URIs cause token validation failures — the `iss` claim in\n> tokens won't match the expected authority.\n\nSee `aspire-configuration` for the general pattern of reading these values via `IOptions<T>`\nin the app project.\n\n> **When generating app code:** The environment variables above map to standard\n> `IConfiguration` keys (`Authentication:Authority`, `Authentication:ClientId`,\n> `Authentication:Scopes:0`, etc.). When scaffolding the web app, configure\n> `AddOpenIdConnect` to read `Authority` and `ClientId` from\n> `builder.Configuration[\"Authentication:Authority\"]` and\n> `builder.Configuration[\"Authentication:ClientId\"]`. Bind scopes from the\n> `Authentication:Scopes` configuration section. For the API, configure\n> `AddJwtBearer` with `options.Authority` from\n> `builder.Configuration[\"Authentication:Authority\"]`.\n> See `aspnetcore-authentication` for full OIDC and JWT Bearer middleware setup.\n> See `aspire-configuration` for the general `IOptions<T>` binding pattern.\n\n---\n\n## Pattern 4: Database and Store Wiring\n\nIdentityServer typically needs its own database for configuration and operational stores.\nOther services in the solution use separate databases for application data.\n\n### Separate databases per service\n\n```csharp\nvar sqlServer = builder.AddSqlServer(\"sql\");\nvar identityDb = sqlServer.AddDatabase(\"identitydb\");\nvar appDb = sqlServer.AddDatabase(\"appdb\");\n\nvar identityServer = builder.AddProject<Projects.IdentityServer>(\"identity-server\")\n    .WithReference(identityDb)\n    .WaitFor(sqlServer);\n\nvar api = builder.AddProject<Projects.WeatherApi>(\"weather-api\")\n    .WithReference(appDb)\n    .WaitFor(sqlServer)\n    .WithReference(identityServer)\n    .WaitFor(identityServer);\n```\n\n`WithReference(identityDb)` sets `ConnectionStrings__identitydb` automatically. The\nIdentityServer project's EF stores must use this connection string name:\n\n```csharp\n// In IdentityServer's Program.cs\nbuilder.Services.AddIdentityServer()\n    .AddConfigurationStore(options =>\n    {\n        options.ConfigureDbContext = b =>\n            b.UseSqlServer(builder.Configuration.GetConnectionString(\"identitydb\"));\n    })\n    .AddOperationalStore(options =>\n    {\n        options.ConfigureDbContext = b =>\n            b.UseSqlServer(builder.Configuration.GetConnectionString(\"identitydb\"));\n    });\n```\n\n### Migration strategies\n\n- **Option A: Startup migration** — Call `Database.MigrateAsync()` in `Program.cs`. Simple\n  and suitable for development.\n- **Option B: Dedicated migration service** — Add a separate migration runner project that\n  runs before IdentityServer:\n\n```csharp\nvar migrations = builder.AddProject<Projects.MigrationRunner>(\"migrations\")\n    .WithReference(identityDb)\n    .WaitFor(sqlServer);\n\nvar identityServer = builder.AddProject<Projects.IdentityServer>(\"identity-server\")\n    .WithReference(identityDb)\n    .WaitFor(migrations);  // Wait for migrations to complete\n```\n\nSee `identityserver-data-storage` for EF Core store configuration details and migration\npatterns.\n\n---\n\n## Pattern 5: Health Checks for Startup Readiness\n\nAspire's `WaitFor()` polls the target's `/health` endpoint. If IdentityServer doesn't\nexpose a health check, `WaitFor()` has no readiness signal and dependent services may\nstart too early or the AppHost may time out waiting.\n\n### Minimal health check setup\n\n```csharp\n// In IdentityServer's Program.cs\nbuilder.Services.AddHealthChecks();\n\n// After building the app\napp.MapHealthChecks(\"/health\");\n```\n\n### Enhanced health checks with readiness validation\n\nFor production-grade startup ordering, add checks that validate IdentityServer can\nactually serve discovery documents and signing keys:\n\n```csharp\nbuilder.Services.AddHealthChecks()\n    .AddCheck(\"self\", () => HealthCheckResult.Healthy(), tags: [\"live\"])\n    .AddCheck<DiscoveryDocumentHealthCheck>(\"discovery\", tags: [\"ready\"])\n    .AddCheck<DiscoveryKeysHealthCheck>(\"jwks\", tags: [\"ready\"]);\n\napp.MapHealthChecks(\"/health\");\napp.MapHealthChecks(\"/alive\", new HealthCheckOptions\n{\n    Predicate = r => r.Tags.Contains(\"live\")\n});\napp.MapHealthChecks(\"/ready\", new HealthCheckOptions\n{\n    Predicate = r => r.Tags.Contains(\"ready\")\n});\n```\n\nThe `DiscoveryDocumentHealthCheck` and `DiscoveryKeysHealthCheck` verify that IdentityServer\ncan serve its discovery document and signing keys. These checks catch configuration errors\n(missing signing credentials, database connection failures) before dependent services try\nto connect.\n\n> **Important:** The `DiscoveryDocumentHealthCheck` and `DiscoveryKeysHealthCheck`\n> implementations are covered in the `identityserver-deployment` skill. Use them to ensure\n> IdentityServer is fully operational before dependent services start.\n\nIf using `AddServiceDefaults()` from the Aspire service defaults project, the `/health`\nand `/alive` endpoints are already mapped. You still need to register the\nIdentityServer-specific health checks in the DI container.\n\n---\n\n## Pattern 6: Service Defaults Integration\n\nIdentityServer emits OpenTelemetry traces and metrics under specific source names. To see\nthem in the Aspire dashboard, add these sources in the shared service defaults project.\n\n### Tracing sources\n\nAdd IdentityServer activity sources to `ConfigureOpenTelemetry` in the service defaults\n`Extensions.cs`:\n\n```csharp\ntracing\n    .AddSource(builder.Environment.ApplicationName)\n    // Duende IdentityServer trace sources\n    .AddSource(\"Duende.IdentityServer\")\n    .AddSource(\"Duende.IdentityServer.Cache\")\n    .AddSource(\"Duende.IdentityServer.Services\")\n    .AddSource(\"Duende.IdentityServer.Stores\")\n    .AddSource(\"Duende.IdentityServer.Validation\")\n    .AddAspNetCoreInstrumentation()\n    .AddHttpClientInstrumentation();\n```\n\n### Metrics\n\nAdd the IdentityServer meter:\n\n```csharp\nmetrics\n    .AddMeter(\"Duende.IdentityServer\")\n    .AddAspNetCoreInstrumentation()\n    .AddHttpClientInstrumentation()\n    .AddRuntimeInstrumentation();\n```\n\n> **Important:** Use string literals (not `IdentityServerConstants.Tracing.*` or\n> `Telemetry.ServiceName`) in service defaults to avoid adding a Duende.IdentityServer\n> package reference to the shared project. Only the IdentityServer project itself should\n> reference the Duende package.\n\nSee `aspire-service-defaults` for the full service defaults setup pattern and\n`identityserver-deployment` for detailed telemetry guidance.\n\n---\n\n## Pattern 7: Multi-Instance Considerations\n\nAspire supports running multiple instances of a project with `WithReplicas`:\n\n```csharp\nvar identityServer = builder.AddProject<Projects.IdentityServer>(\"identity-server\")\n    .WithReference(identityDb)\n    .WaitFor(sqlServer)\n    .WithReplicas(3);\n```\n\nRunning multiple IdentityServer instances requires shared state across all replicas:\n\n- **Shared signing key store** — All instances must access the same signing keys via a\n  shared `ISigningKeyStore` (EF operational store or custom implementation)\n- **Shared data protection keys** — All instances must share ASP.NET Data Protection keys\n  (Redis, database, or blob storage). Without this, authentication cookies encrypted by\n  one instance can't be decrypted by another.\n- **Shared operational store** — Persisted grants, device codes, and server-side sessions\n  must be in a shared database\n- **Distributed cache** — Required if using the OIDC state data formatter, JWT replay\n  cache, or Pushed Authorization Requests (PAR)\n\nSee `identityserver-deployment` for data protection and operational store configuration.\nSee `identityserver-data-storage` for EF Core store setup.\n\n> **Important:** Do NOT use `.WithReplicas(n)` without first configuring shared state.\n> Multiple instances with file-based signing keys or in-memory stores will produce token\n> validation failures, lost sessions, and authentication cookie errors.\n\n---\n\n## Pattern 8: Integration Testing with IdentityServer\n\nWhen integration testing an Aspire solution that includes IdentityServer, the key\nchallenge is that the authority URL uses a dynamic port assigned at runtime. Test clients\nmust discover the URL from the test fixture.\n\n```csharp\npublic sealed class IdentityAspireFixture : IAsyncLifetime\n{\n    private DistributedApplication? _app;\n\n    public async Task InitializeAsync()\n    {\n        var builder = await DistributedApplicationTestingBuilder\n            .CreateAsync<Projects.MyApp_AppHost>();\n\n        _app = await builder.BuildAsync();\n        await _app.StartAsync();\n\n        // Wait for IdentityServer to be healthy before running tests\n        await _app.ResourceNotifications\n            .WaitForResourceHealthyAsync(\"identity-server\");\n    }\n\n    public Uri GetAuthorityUrl() =>\n        _app!.GetEndpoint(\"identity-server\", \"https\");\n\n    public HttpClient CreateApiClient() =>\n        _app!.CreateHttpClient(\"weather-api\");\n\n    public async Task DisposeAsync()\n    {\n        if (_app is not null)\n        {\n            await _app.StopAsync();\n            await _app.DisposeAsync();\n        }\n    }\n}\n```\n\nThe important details:\n\n- **`WaitForResourceHealthyAsync(\"identity-server\")`** ensures IdentityServer is fully\n  ready before any test runs. The resource name matches the name in the AppHost.\n- **`GetEndpoint(\"identity-server\", \"https\")`** returns the dynamic `https://localhost:{port}`\n  URL. Use this as the authority when configuring test `HttpClient` instances.\n- **`CreateHttpClient(\"weather-api\")`** creates a client pre-configured with the API's\n  dynamic base address.\n\n---\n\n## Do / Don't Checklist\n\n**Do**\n- Use `WithReference()` + `WaitFor()` for every service that depends on IdentityServer\n- Pass authority URLs and OIDC settings as explicit environment variables\n- Register health checks in IdentityServer for Aspire startup ordering\n- Add IdentityServer telemetry sources to service defaults as string literals\n- Use separate databases for IdentityServer and application data\n\n**Don't**\n- Start APIs or web apps without `WaitFor(identityServer)` — causes discovery failures\n- Reference Duende packages from the shared service defaults project\n- Use `WithReplicas()` without configuring shared state (signing keys, data protection, operational store)\n- Set `IssuerUri` unless the internal and external URLs actually differ\n- Duplicate Aspire or IdentityServer patterns covered by other skills — cross-reference them\n\n---\n\n## Resources\n- .NET Aspire orchestration: https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/app-host-overview\n- Aspire service dependencies: https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/app-host-overview#waiting-for-resources\n- Duende IdentityServer documentation: https://docs.duendesoftware.com/\n"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}