--- 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. invocable: false --- # Setting Up and Hosting IdentityServer ## When to Use This Skill - Setting up a new Duende IdentityServer project from scratch - Configuring the ASP.NET Core DI system and middleware pipeline for IdentityServer - Deciding between separate vs shared hosting patterns - Integrating IdentityServer with ASP.NET Identity for user management - Configuring `IdentityServerOptions` (issuer, key management, endpoints) - Setting up proxy/load balancer forwarded headers - Configuring data protection for production deployments - Understanding the IdentityServer middleware pipeline ordering Docs: https://docs.duendesoftware.com/identityserver/fundamentals ## Core Concepts Duende 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. ### Architecture Decision: Separate vs Shared Host IdentityServer 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. | Hosting Pattern | Pros | Cons | | ------------------------------- | -------------------------------------------------------------------- | ------------------------------------------- | | **Separate host (recommended)** | Minimal attack surface, independent scaling, clear security boundary | Additional deployment artifact | | **Shared with web app** | Fewer projects | Larger attack surface, coupled deployments | | **Shared with API** | Fewer projects | Security risk, conflicting middleware needs | ## Step 1: Install Templates and Create a Project ```bash dotnet new install Duende.Templates dotnet new duende-is-empty -n IdentityServer ``` The `duende-is-empty` template creates a minimal project with the IdentityServer NuGet package installed and basic configuration. ## Step 2: Register IdentityServer Services (DI) Call `AddIdentityServer` on the service collection to register all necessary services. This method also calls `AddAuthentication` internally. ```csharp // Program.cs var idsvrBuilder = builder.Services.AddIdentityServer(options => { // Configure IdentityServerOptions here }); ``` ### Adding Configuration Stores The builder object returned by `AddIdentityServer` provides extension methods to add configuration stores for clients, resources, and scopes: ```csharp // Program.cs var idsvrBuilder = builder.Services.AddIdentityServer() .AddInMemoryClients(Config.Clients) .AddInMemoryIdentityResources(Config.IdentityResources) .AddInMemoryApiScopes(Config.ApiScopes); ``` **Store options:** - **In-memory stores** - good for development, demos, and static configuration - **EntityFramework stores** - production-ready, supports dynamic configuration - **Custom stores** - implement the store interfaces for any backing store ### Minimal Working Example ```csharp // Program.cs builder.Services.AddIdentityServer() .AddInMemoryApiScopes(Config.ApiScopes) .AddInMemoryClients(Config.Clients); var app = builder.Build(); app.UseStaticFiles(); app.UseRouting(); app.UseIdentityServer(); app.UseAuthorization(); app.MapDefaultControllerRoute(); app.Run(); ``` ## Step 3: Configure the Request Pipeline Add `UseIdentityServer` middleware to the pipeline. Pipeline ordering is critical. ```csharp // Program.cs var app = builder.Build(); app.UseStaticFiles(); app.UseRouting(); app.UseIdentityServer(); app.UseAuthorization(); app.MapDefaultControllerRoute(); ``` ### Pipeline Ordering Rules | Order | Middleware | Notes | | ----- | ----------------------------- | -------------------------------------------------- | | 1 | `UseStaticFiles()` | Before IdentityServer | | 2 | `UseRouting()` | Before IdentityServer | | 3 | `UseIdentityServer()` | Includes `UseAuthentication()` internally | | 4 | `UseAuthorization()` | Required after IdentityServer, must not be omitted | | 5 | `MapDefaultControllerRoute()` | UI framework endpoints | ### Common Pipeline Anti-Patterns ```csharp // ❌ WRONG: UseAuthentication is redundant (UseIdentityServer includes it) app.UseAuthentication(); app.UseIdentityServer(); // ✅ CORRECT: UseIdentityServer already calls UseAuthentication app.UseIdentityServer(); app.UseAuthorization(); ``` ```csharp // ❌ WRONG: Missing UseAuthorization - required for the Duende UI template app.UseIdentityServer(); app.MapDefaultControllerRoute(); // ✅ CORRECT: Always include UseAuthorization after UseIdentityServer app.UseIdentityServer(); app.UseAuthorization(); app.MapDefaultControllerRoute(); ``` ```csharp // ❌ WRONG: IdentityServer before routing app.UseIdentityServer(); app.UseRouting(); // ✅ CORRECT: Routing before IdentityServer app.UseRouting(); app.UseIdentityServer(); ``` ## Step 4: Configure Essential IdentityServerOptions ```csharp // Program.cs var idsvrBuilder = builder.Services.AddIdentityServer(options => { // IssuerUri: Not recommended to set; inferred from request URL by default. // Set only when IdentityServer is accessed on a different address than the // expected issuer (e.g., internal Kubernetes address). // options.IssuerUri = "https://identity.example.com"; // Emit scopes as space-delimited string per RFC 9068 options.EmitScopesAsSpaceDelimitedStringInJwt = false; // default, array format // Emit static audience claim in format {issuer}/resources options.EmitStaticAudienceClaim = false; // default // Emit iss response parameter on authorize responses (RFC 9207) options.EmitIssuerIdentificationResponseParameter = true; // default }); ``` ### Key Configuration Properties | Property | Default | Purpose | | ------------------------------------------- | ----------------- | ------------------------------------------------- | | `IssuerUri` | inferred from URL | Token issuer name in discovery and tokens | | `LowerCaseIssuerUri` | `true` | Lowercase inferred issuer URIs | | `AccessTokenJwtType` | `"at+jwt"` | `typ` header in JWT access tokens (RFC 9068) | | `EmitScopesAsSpaceDelimitedStringInJwt` | `false` | Scope claim format in JWTs | | `EmitStaticAudienceClaim` | `false` | Static `aud` claim in `{issuer}/resources` format | | `EmitIssuerIdentificationResponseParameter` | `true` | `iss` param on authorize responses (RFC 9207) | ## Step 5: Configure the License Key Duende 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. Set the license key via `options.LicenseKey` or via configuration: ```csharp // Option 1: Inline in AddIdentityServer (not recommended for production — keep out of source control) builder.Services.AddIdentityServer(options => { options.LicenseKey = "YOUR_LICENSE_KEY"; }); // Option 2: From configuration (recommended) builder.Services.AddIdentityServer(options => { options.LicenseKey = builder.Configuration["IdentityServer:LicenseKey"]; }); ``` Store the key in a secret manager, environment variable, or key vault — never in source-controlled `appsettings.json`. ## Step 6: ASP.NET Identity Integration To use ASP.NET Identity as the user store for IdentityServer, install the integration package and configure both systems: ```bash dotnet add package Duende.IdentityServer.AspNetIdentity ``` ```csharp // Program.cs builder.Services.AddIdentity() .AddEntityFrameworkStores() .AddDefaultTokenProviders(); builder.Services.AddIdentityServer() .AddAspNetIdentity(); ``` ### What AddAspNetIdentity Configures `AddAspNetIdentity` registers the following IdentityServer implementations: - **`IProfileService`** - uses `IUserClaimsPrincipalFactory` to add claims to tokens - **`IResourceOwnerPasswordValidator`** - supports the password grant type - **`IUserClaimsPrincipalFactory`** - a wrapper implementation that calls through to the previously registered factory and adds extra IdentityServer-specific claims ### Custom IUserClaimsPrincipalFactory If 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: ```csharp // Program.cs // Register custom factory BEFORE AddAspNetIdentity builder.Services.AddScoped, CustomClaimsPrincipalFactory>(); builder.Services.AddIdentityServer() .AddAspNetIdentity(); ``` ### Inactive User Handling ASP.NET Identity has no built-in concept of inactive users. The default `IsActiveAsync` implementation returns `true`. To support enable/disable functionality: ```csharp public class CustomProfileService : ProfileService { public CustomProfileService( UserManager userManager, IUserClaimsPrincipalFactory claimsFactory) : base(userManager, claimsFactory) { } protected override Task IsUserActiveAsync(ApplicationUser user) { return Task.FromResult(user.IsEnabled); // your custom property } } ``` ### Template Alternative Use the `duende-is-aspid` template for a pre-configured ASP.NET Identity integration: ```bash dotnet new duende-is-aspid -n IdentityServer ``` ## Production Deployment: Proxy and Load Balancer Configuration When behind a reverse proxy or load balancer, the proxy obscures request scheme and IP address. This causes common symptoms: - HTTPS downgraded to HTTP in discovery document - Incorrect host names in discovery or redirects - Cookies missing the `secure` attribute ### Solution: Forwarded Headers Middleware **Option 1: Environment variable (simple)** Set `ASPNETCORE_FORWARDEDHEADERS_ENABLED=true` for cloud/Kubernetes environments. **Option 2: Explicit configuration (production)** ```csharp // Program.cs builder.Services.Configure(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedHost | ForwardedHeaders.XForwardedProto; options.KnownProxies.Add(IPAddress.Parse("203.0.113.42")); options.ForwardLimit = 1; }); ``` Add `UseForwardedHeaders()` early in the pipeline, before `UseIdentityServer()`. ## Production Deployment: Data Protection Data 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. ```csharp // Program.cs builder.Services.AddDataProtection() .PersistKeysToFoo() // Choose persistence (FileSystem, DbContext, Azure, Redis, etc.) .ProtectKeysWithBar() // Choose key protection (Certificate, Azure Key Vault, etc.) .SetApplicationName("My.IdentityServer"); // Prevent key isolation issues ``` ### Data Protection Checklist | Requirement | Why | | ----------------------------------------- | ------------------------------------------------------------ | | Persist keys to durable storage | Keys are lost on restart without persistence | | Share keys across load-balanced instances | Each instance must read data protected by other instances | | Set explicit application name | Prevents key isolation across deployments | | Ensure storage durability | Redis without persistence or ephemeral filesystems lose keys | ### Data Protection Keys vs Signing Keys These are completely separate: | | Data Protection Keys | IdentityServer Signing Keys | | ---------------- | --------------------------------------------- | ------------------------------------ | | **Purpose** | Encrypt/sign sensitive data (cookies, grants) | Sign tokens (JWT, id_token) | | **Cryptography** | Symmetric (private key) | Asymmetric (public/private key pair) | | **Framework** | ASP.NET Core Data Protection | IdentityServer Key Management | | **Public** | No | Public keys published in discovery | ## Common Pitfalls 1. **Missing `UseAuthorization()`** - The Duende UI template requires authorization middleware. Omitting it causes authorization failures in the UI pages. 2. **Redundant `UseAuthentication()`** - `UseIdentityServer()` already includes `UseAuthentication()`. Adding both is unnecessary but not harmful. 3. **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. 4. **Issuer mismatch** - If `IssuerUri` is set manually, clients must know this exact value. Prefer letting IdentityServer infer the issuer from request URLs. 5. **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`. 6. **Shared hosting with APIs/clients** - Co-hosting IdentityServer with other applications increases the attack surface. Use a dedicated host. 7. **Not calling `AddAspNetIdentity` after `AddIdentity`** - When using ASP.NET Identity, you must call both. `AddIdentity` configures ASP.NET Identity; `AddAspNetIdentity` bridges it to IdentityServer. --- ## Related Skills - `identityserver-configuration` — client definitions, resources, scopes - `identityserver-deployment` — production deployment, data protection, health checks - `identityserver-aspire` — orchestrating IdentityServer in Aspire AppHost