← Files Duende SkillsARCHIVED FILE
skills/identityserver-hosting-setup/SKILL.md
14.7 KB · Oct 2, 2026 · 00:31 UTC
---
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<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
builder.Services.AddIdentityServer()
.AddAspNetIdentity<ApplicationUser>();
```
### What AddAspNetIdentity Configures
`AddAspNetIdentity<TUser>` 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<IUserClaimsPrincipalFactory<ApplicationUser>, CustomClaimsPrincipalFactory>();
builder.Services.AddIdentityServer()
.AddAspNetIdentity<ApplicationUser>();
```
### 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<ApplicationUser>
{
public CustomProfileService(
UserManager<ApplicationUser> userManager,
IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory)
: base(userManager, claimsFactory)
{ }
protected override Task<bool> 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<ForwardedHeadersOptions>(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
SHA-256: 20c4e1efafe8b19ab59b2b3ec03dc169ffceb459764fe493a0bfdf5b4e494927