{"id":18163,"plugin_id":"plugins_6a86acf7816881918552f3b43bc0db69","kind":"skill","collection_source":null,"comparison_source":null,"observed_at":"2026-09-30T23:14:41.187Z","digest":"6b908003c3c7bde015ac1503258a703214acda308887bfa1c5766d16a81ed8af","against":null,"payload":{"name":"identityserver-hosting-setup","description":"Setting up and hosting Duende IdentityServer in ASP.NET Core applications, including DI registration, middleware pipeline, hosting patterns, essential options, license configuration, and ASP.NET Identity integration.","included_files":[],"skill_md_contents":"---\nname: identityserver-hosting-setup\ndescription: Setting up and hosting Duende IdentityServer in ASP.NET Core applications, including DI registration, middleware pipeline, hosting patterns, essential options, license configuration, and ASP.NET Identity integration.\ninvocable: false\n---\n\n# Setting Up and Hosting IdentityServer\n\n## When to Use This Skill\n\n- Setting up a new Duende IdentityServer project from scratch\n- Configuring the ASP.NET Core DI system and middleware pipeline for IdentityServer\n- Deciding between separate vs shared hosting patterns\n- Integrating IdentityServer with ASP.NET Identity for user management\n- Configuring `IdentityServerOptions` (issuer, key management, endpoints)\n- Setting up proxy/load balancer forwarded headers\n- Configuring data protection for production deployments\n- Understanding the IdentityServer middleware pipeline ordering\n\nDocs: https://docs.duendesoftware.com/identityserver/fundamentals\n\n## Core Concepts\n\nDuende IdentityServer is middleware that adds OpenID Connect and OAuth 2.0 endpoints to an ASP.NET Core host. It requires two setup steps: registering services in DI and adding middleware to the request pipeline.\n\n### Architecture Decision: Separate vs Shared Host\n\nIdentityServer should be in its own dedicated application to minimize the attack surface. While it is technically possible to co-host IdentityServer with clients or APIs, this is not recommended.\n\n| Hosting Pattern                 | Pros                                                                 | Cons                                        |\n| ------------------------------- | -------------------------------------------------------------------- | ------------------------------------------- |\n| **Separate host (recommended)** | Minimal attack surface, independent scaling, clear security boundary | Additional deployment artifact              |\n| **Shared with web app**         | Fewer projects                                                       | Larger attack surface, coupled deployments  |\n| **Shared with API**             | Fewer projects                                                       | Security risk, conflicting middleware needs |\n\n## Step 1: Install Templates and Create a Project\n\n```bash\ndotnet new install Duende.Templates\ndotnet new duende-is-empty -n IdentityServer\n```\n\nThe `duende-is-empty` template creates a minimal project with the IdentityServer NuGet package installed and basic configuration.\n\n## Step 2: Register IdentityServer Services (DI)\n\nCall `AddIdentityServer` on the service collection to register all necessary services. This method also calls `AddAuthentication` internally.\n\n```csharp\n// Program.cs\nvar idsvrBuilder = builder.Services.AddIdentityServer(options =>\n{\n    // Configure IdentityServerOptions here\n});\n```\n\n### Adding Configuration Stores\n\nThe builder object returned by `AddIdentityServer` provides extension methods to add configuration stores for clients, resources, and scopes:\n\n```csharp\n// Program.cs\nvar idsvrBuilder = builder.Services.AddIdentityServer()\n    .AddInMemoryClients(Config.Clients)\n    .AddInMemoryIdentityResources(Config.IdentityResources)\n    .AddInMemoryApiScopes(Config.ApiScopes);\n```\n\n**Store options:**\n\n- **In-memory stores** - good for development, demos, and static configuration\n- **EntityFramework stores** - production-ready, supports dynamic configuration\n- **Custom stores** - implement the store interfaces for any backing store\n\n### Minimal Working Example\n\n```csharp\n// Program.cs\nbuilder.Services.AddIdentityServer()\n    .AddInMemoryApiScopes(Config.ApiScopes)\n    .AddInMemoryClients(Config.Clients);\n\nvar app = builder.Build();\n\napp.UseStaticFiles();\napp.UseRouting();\napp.UseIdentityServer();\napp.UseAuthorization();\napp.MapDefaultControllerRoute();\n\napp.Run();\n```\n\n## Step 3: Configure the Request Pipeline\n\nAdd `UseIdentityServer` middleware to the pipeline. Pipeline ordering is critical.\n\n```csharp\n// Program.cs\nvar app = builder.Build();\napp.UseStaticFiles();\n\napp.UseRouting();\napp.UseIdentityServer();\napp.UseAuthorization();\n\napp.MapDefaultControllerRoute();\n```\n\n### Pipeline Ordering Rules\n\n| Order | Middleware                    | Notes                                              |\n| ----- | ----------------------------- | -------------------------------------------------- |\n| 1     | `UseStaticFiles()`            | Before IdentityServer                              |\n| 2     | `UseRouting()`                | Before IdentityServer                              |\n| 3     | `UseIdentityServer()`         | Includes `UseAuthentication()` internally          |\n| 4     | `UseAuthorization()`          | Required after IdentityServer, must not be omitted |\n| 5     | `MapDefaultControllerRoute()` | UI framework endpoints                             |\n\n### Common Pipeline Anti-Patterns\n\n```csharp\n// ❌ WRONG: UseAuthentication is redundant (UseIdentityServer includes it)\napp.UseAuthentication();\napp.UseIdentityServer();\n\n// ✅ CORRECT: UseIdentityServer already calls UseAuthentication\napp.UseIdentityServer();\napp.UseAuthorization();\n```\n\n```csharp\n// ❌ WRONG: Missing UseAuthorization - required for the Duende UI template\napp.UseIdentityServer();\napp.MapDefaultControllerRoute();\n\n// ✅ CORRECT: Always include UseAuthorization after UseIdentityServer\napp.UseIdentityServer();\napp.UseAuthorization();\napp.MapDefaultControllerRoute();\n```\n\n```csharp\n// ❌ WRONG: IdentityServer before routing\napp.UseIdentityServer();\napp.UseRouting();\n\n// ✅ CORRECT: Routing before IdentityServer\napp.UseRouting();\napp.UseIdentityServer();\n```\n\n## Step 4: Configure Essential IdentityServerOptions\n\n```csharp\n// Program.cs\nvar idsvrBuilder = builder.Services.AddIdentityServer(options =>\n{\n    // IssuerUri: Not recommended to set; inferred from request URL by default.\n    // Set only when IdentityServer is accessed on a different address than the\n    // expected issuer (e.g., internal Kubernetes address).\n    // options.IssuerUri = \"https://identity.example.com\";\n\n    // Emit scopes as space-delimited string per RFC 9068\n    options.EmitScopesAsSpaceDelimitedStringInJwt = false; // default, array format\n\n    // Emit static audience claim in format {issuer}/resources\n    options.EmitStaticAudienceClaim = false; // default\n\n    // Emit iss response parameter on authorize responses (RFC 9207)\n    options.EmitIssuerIdentificationResponseParameter = true; // default\n});\n```\n\n### Key Configuration Properties\n\n| Property                                    | Default           | Purpose                                           |\n| ------------------------------------------- | ----------------- | ------------------------------------------------- |\n| `IssuerUri`                                 | inferred from URL | Token issuer name in discovery and tokens         |\n| `LowerCaseIssuerUri`                        | `true`            | Lowercase inferred issuer URIs                    |\n| `AccessTokenJwtType`                        | `\"at+jwt\"`        | `typ` header in JWT access tokens (RFC 9068)      |\n| `EmitScopesAsSpaceDelimitedStringInJwt`     | `false`           | Scope claim format in JWTs                        |\n| `EmitStaticAudienceClaim`                   | `false`           | Static `aud` claim in `{issuer}/resources` format |\n| `EmitIssuerIdentificationResponseParameter` | `true`            | `iss` param on authorize responses (RFC 9207)     |\n\n## Step 5: Configure the License Key\n\nDuende IdentityServer requires a valid license for production use. Without a license key, IdentityServer runs in trial/community mode and will log a warning on startup.\n\nSet the license key via `options.LicenseKey` or via configuration:\n\n```csharp\n// Option 1: Inline in AddIdentityServer (not recommended for production — keep out of source control)\nbuilder.Services.AddIdentityServer(options =>\n{\n    options.LicenseKey = \"YOUR_LICENSE_KEY\";\n});\n\n// Option 2: From configuration (recommended)\nbuilder.Services.AddIdentityServer(options =>\n{\n    options.LicenseKey = builder.Configuration[\"IdentityServer:LicenseKey\"];\n});\n```\n\nStore the key in a secret manager, environment variable, or key vault — never in source-controlled `appsettings.json`.\n\n## Step 6: ASP.NET Identity Integration\n\nTo use ASP.NET Identity as the user store for IdentityServer, install the integration package and configure both systems:\n\n```bash\ndotnet add package Duende.IdentityServer.AspNetIdentity\n```\n\n```csharp\n// Program.cs\nbuilder.Services.AddIdentity<ApplicationUser, IdentityRole>()\n    .AddEntityFrameworkStores<ApplicationDbContext>()\n    .AddDefaultTokenProviders();\n\nbuilder.Services.AddIdentityServer()\n    .AddAspNetIdentity<ApplicationUser>();\n```\n\n### What AddAspNetIdentity Configures\n\n`AddAspNetIdentity<TUser>` registers the following IdentityServer implementations:\n\n- **`IProfileService`** - uses `IUserClaimsPrincipalFactory` to add claims to tokens\n- **`IResourceOwnerPasswordValidator`** - supports the password grant type\n- **`IUserClaimsPrincipalFactory`** - a wrapper implementation that calls through to the previously registered factory and adds extra IdentityServer-specific claims\n\n### Custom IUserClaimsPrincipalFactory\n\nIf you register a custom `IUserClaimsPrincipalFactory` before calling `AddAspNetIdentity`, the IdentityServer registration will resolve your factory and call through to it, layering additional claims on top:\n\n```csharp\n// Program.cs\n\n// Register custom factory BEFORE AddAspNetIdentity\nbuilder.Services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, CustomClaimsPrincipalFactory>();\n\nbuilder.Services.AddIdentityServer()\n    .AddAspNetIdentity<ApplicationUser>();\n```\n\n### Inactive User Handling\n\nASP.NET Identity has no built-in concept of inactive users. The default `IsActiveAsync` implementation returns `true`. To support enable/disable functionality:\n\n```csharp\npublic class CustomProfileService : ProfileService<ApplicationUser>\n{\n    public CustomProfileService(\n        UserManager<ApplicationUser> userManager,\n        IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory)\n        : base(userManager, claimsFactory)\n    { }\n\n    protected override Task<bool> IsUserActiveAsync(ApplicationUser user)\n    {\n        return Task.FromResult(user.IsEnabled); // your custom property\n    }\n}\n```\n\n### Template Alternative\n\nUse the `duende-is-aspid` template for a pre-configured ASP.NET Identity integration:\n\n```bash\ndotnet new duende-is-aspid -n IdentityServer\n```\n\n## Production Deployment: Proxy and Load Balancer Configuration\n\nWhen behind a reverse proxy or load balancer, the proxy obscures request scheme and IP address. This causes common symptoms:\n\n- HTTPS downgraded to HTTP in discovery document\n- Incorrect host names in discovery or redirects\n- Cookies missing the `secure` attribute\n\n### Solution: Forwarded Headers Middleware\n\n**Option 1: Environment variable (simple)**\nSet `ASPNETCORE_FORWARDEDHEADERS_ENABLED=true` for cloud/Kubernetes environments.\n\n**Option 2: Explicit configuration (production)**\n\n```csharp\n// Program.cs\nbuilder.Services.Configure<ForwardedHeadersOptions>(options =>\n{\n    options.ForwardedHeaders = ForwardedHeaders.XForwardedHost |\n                                ForwardedHeaders.XForwardedProto;\n\n    options.KnownProxies.Add(IPAddress.Parse(\"203.0.113.42\"));\n    options.ForwardLimit = 1;\n});\n```\n\nAdd `UseForwardedHeaders()` early in the pipeline, before `UseIdentityServer()`.\n\n## Production Deployment: Data Protection\n\nData protection is critical for IdentityServer. It protects signing keys at rest, persisted grants, server-side sessions, and authentication cookies. See [ASP.NET Core Data Protection](https://docs.duendesoftware.com/general/data-protection/) for comprehensive guidance covering all Duende SDKs.\n\n```csharp\n// Program.cs\nbuilder.Services.AddDataProtection()\n    .PersistKeysToFoo()       // Choose persistence (FileSystem, DbContext, Azure, Redis, etc.)\n    .ProtectKeysWithBar()     // Choose key protection (Certificate, Azure Key Vault, etc.)\n    .SetApplicationName(\"My.IdentityServer\"); // Prevent key isolation issues\n```\n\n### Data Protection Checklist\n\n| Requirement                               | Why                                                          |\n| ----------------------------------------- | ------------------------------------------------------------ |\n| Persist keys to durable storage           | Keys are lost on restart without persistence                 |\n| Share keys across load-balanced instances | Each instance must read data protected by other instances    |\n| Set explicit application name             | Prevents key isolation across deployments                    |\n| Ensure storage durability                 | Redis without persistence or ephemeral filesystems lose keys |\n\n### Data Protection Keys vs Signing Keys\n\nThese are completely separate:\n\n|                  | Data Protection Keys                          | IdentityServer Signing Keys          |\n| ---------------- | --------------------------------------------- | ------------------------------------ |\n| **Purpose**      | Encrypt/sign sensitive data (cookies, grants) | Sign tokens (JWT, id_token)          |\n| **Cryptography** | Symmetric (private key)                       | Asymmetric (public/private key pair) |\n| **Framework**    | ASP.NET Core Data Protection                  | IdentityServer Key Management        |\n| **Public**       | No                                            | Public keys published in discovery   |\n\n## Common Pitfalls\n\n1. **Missing `UseAuthorization()`** - The Duende UI template requires authorization middleware. Omitting it causes authorization failures in the UI pages.\n\n2. **Redundant `UseAuthentication()`** - `UseIdentityServer()` already includes `UseAuthentication()`. Adding both is unnecessary but not harmful.\n\n3. **Data protection not configured for production** - The default file-based key storage does not survive container restarts or work across load-balanced instances. Always configure persistent, shared key storage.\n\n4. **Issuer mismatch** - If `IssuerUri` is set manually, clients must know this exact value. Prefer letting IdentityServer infer the issuer from request URLs.\n\n5. **Keys directory in source control** - The `~/keys` directory created by automatic key management contains cryptographic secrets and must be excluded from source control via `.gitignore`.\n\n6. **Shared hosting with APIs/clients** - Co-hosting IdentityServer with other applications increases the attack surface. Use a dedicated host.\n\n7. **Not calling `AddAspNetIdentity` after `AddIdentity`** - When using ASP.NET Identity, you must call both. `AddIdentity` configures ASP.NET Identity; `AddAspNetIdentity` bridges it to IdentityServer.\n\n---\n\n## Related Skills\n\n- `identityserver-configuration` — client definitions, resources, scopes\n- `identityserver-deployment` — production deployment, data protection, health checks\n- `identityserver-aspire` — orchestrating IdentityServer in Aspire AppHost\n"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}