From duende-skills
Configures and hosts Duende IdentityServer in ASP.NET Core, covering DI registration, middleware pipeline, hosting patterns, options, license, and ASP.NET Identity integration.
How this skill is triggered — by the user, by Claude, or both
Slash command
/duende-skills:identityserver-hosting-setupThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
- Setting up a new Duende IdentityServer project from scratch
IdentityServerOptions (issuer, key management, endpoints)Docs: https://docs.duendesoftware.com/identityserver/fundamentals
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.
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 |
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.
Call AddIdentityServer on the service collection to register all necessary services. This method also calls AddAuthentication internally.
// Program.cs
var idsvrBuilder = builder.Services.AddIdentityServer(options =>
{
// Configure IdentityServerOptions here
});
The builder object returned by AddIdentityServer provides extension methods to add configuration stores for clients, resources, and scopes:
// Program.cs
var idsvrBuilder = builder.Services.AddIdentityServer()
.AddInMemoryClients(Config.Clients)
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryApiScopes(Config.ApiScopes);
Store options:
// 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();
Add UseIdentityServer middleware to the pipeline. Pipeline ordering is critical.
// Program.cs
var app = builder.Build();
app.UseStaticFiles();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthorization();
app.MapDefaultControllerRoute();
| 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 |
// ❌ WRONG: UseAuthentication is redundant (UseIdentityServer includes it)
app.UseAuthentication();
app.UseIdentityServer();
// ✅ CORRECT: UseIdentityServer already calls UseAuthentication
app.UseIdentityServer();
app.UseAuthorization();
// ❌ 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();
// ❌ WRONG: IdentityServer before routing
app.UseIdentityServer();
app.UseRouting();
// ✅ CORRECT: Routing before IdentityServer
app.UseRouting();
app.UseIdentityServer();
// 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
});
| 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) |
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:
// 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.
To use ASP.NET Identity as the user store for IdentityServer, install the integration package and configure both systems:
dotnet add package Duende.IdentityServer.AspNetIdentity
// Program.cs
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
builder.Services.AddIdentityServer()
.AddAspNetIdentity<ApplicationUser>();
AddAspNetIdentity<TUser> registers the following IdentityServer implementations:
IProfileService - uses IUserClaimsPrincipalFactory to add claims to tokensIResourceOwnerPasswordValidator - supports the password grant typeIUserClaimsPrincipalFactory - a wrapper implementation that calls through to the previously registered factory and adds extra IdentityServer-specific claimsIf 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:
// Program.cs
// Register custom factory BEFORE AddAspNetIdentity
builder.Services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, CustomClaimsPrincipalFactory>();
builder.Services.AddIdentityServer()
.AddAspNetIdentity<ApplicationUser>();
ASP.NET Identity has no built-in concept of inactive users. The default IsActiveAsync implementation returns true. To support enable/disable functionality:
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
}
}
Use the duende-is-aspid template for a pre-configured ASP.NET Identity integration:
dotnet new duende-is-aspid -n IdentityServer
When behind a reverse proxy or load balancer, the proxy obscures request scheme and IP address. This causes common symptoms:
secure attributeOption 1: Environment variable (simple)
Set ASPNETCORE_FORWARDEDHEADERS_ENABLED=true for cloud/Kubernetes environments.
Option 2: Explicit configuration (production)
// 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().
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 for comprehensive guidance covering all Duende SDKs.
// 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
| 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 |
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 |
Missing UseAuthorization() - The Duende UI template requires authorization middleware. Omitting it causes authorization failures in the UI pages.
Redundant UseAuthentication() - UseIdentityServer() already includes UseAuthentication(). Adding both is unnecessary but not harmful.
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.
Issuer mismatch - If IssuerUri is set manually, clients must know this exact value. Prefer letting IdentityServer infer the issuer from request URLs.
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.
Shared hosting with APIs/clients - Co-hosting IdentityServer with other applications increases the attack surface. Use a dedicated host.
Not calling AddAspNetIdentity after AddIdentity - When using ASP.NET Identity, you must call both. AddIdentity configures ASP.NET Identity; AddAspNetIdentity bridges it to IdentityServer.
identityserver-configuration — client definitions, resources, scopesidentityserver-deployment — production deployment, data protection, health checksidentityserver-aspire — orchestrating IdentityServer in Aspire AppHostProvides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.
npx claudepluginhub duendesoftware/duende-skills --plugin duende-skills