← Duende SkillsCONTENT HISTORYWHAT CHANGED · RULE-BASED ANALYSIS
Update to Duende Skills
Snapshot Sep 30, 2026 · 23:14 UTC · version 0.3.0
Collection source: not recorded for this historical snapshot.
First saved snapshot
No earlier snapshot is available to establish a change.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"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"
}SHA-256: 6b908003c3c7bde015ac1503258a703214acda308887bfa1c5766d16a81ed8af