← Files Duende SkillsARCHIVED FILE
skills/identityserver-configuration/SKILL.md
21.2 KB · Oct 5, 2026 · 18:31 UTC
---
name: identityserver-configuration
description: Configure Duende IdentityServer including client definitions, API resources, identity resources, scopes, signing credentials, and server-side sessions. Covers client types (M2M, interactive, SPA), grant types, API Scopes vs API Resources vs Identity Resources, secret management, and client authentication methods. Includes both in-memory and database-backed configuration.
invocable: false
---
# Duende IdentityServer Configuration
## When to Use This Skill
Use this skill when:
- Setting up a new Duende IdentityServer host
- Defining or modifying client registrations
- Configuring API resources, API scopes, or identity resources
- Setting up signing key management (automatic or static)
- Enabling server-side sessions
- Tuning `IdentityServerOptions` for production deployments
- Migrating from IdentityServer4 to Duende IdentityServer
## Core Principles
1. **Authorization Code + PKCE by Default** — Use `GrantTypes.Code` for all interactive clients. Never use implicit flow for new applications.
2. **Least Privilege Scopes** — Grant clients only the scopes they need. Avoid wildcard or overly broad scope assignments.
3. **Automatic Key Management** — Prefer the built-in automatic key rotation over static key configuration in production.
4. **API Resources for Audience Isolation** — Use `ApiResource` to control the `aud` claim and isolate API boundaries. Use `ApiScope` for fine-grained permission modeling within those boundaries.
5. **Server-Side Sessions for Enterprise** — Enable server-side sessions when you need centralized session management, back-channel logout, or session queries.
## Related Skills
- `identityserver-stores` — EF Core persistence for configuration and operational data
- `oauth-oidc-protocols` — Protocol fundamentals that underpin these configuration choices
- `identity-security-hardening` — Production hardening of IdentityServer deployments
- `token-management` — Client-side token lifecycle with Duende.AccessTokenManagement
- `aspnetcore-authentication` — Configuring OIDC authentication in client applications
Docs: https://docs.duendesoftware.com/identityserver/configuration
---
## Sub-Documents
Load these sub-documents when the user's question specifically targets one of these areas:
| Document | Description | When to Load |
|----------|-------------|--------------|
| [docs/client-types.md](docs/client-types.md) | Grant type selection matrix, client property reference tables, client authentication methods (shared secret, private_key_jwt, mTLS), secret rollover, and CORS | private_key_jwt, mTLS, secret rotation, refresh token settings, client authentication, CORS origins |
| [docs/resources-scopes.md](docs/resources-scopes.md) | Resource type decision matrix, identity resources, API scopes (including parameterized scopes), and API resources with audience isolation | aud claim, audience isolation, parameterized scopes, IScopeParser, IResourceValidator, EmitStaticAudienceClaim, API Resources, Identity Resources |
---
## Pattern 1: Hosting and Basic Setup
Register Duende IdentityServer in `Program.cs` with `AddIdentityServer`. All configuration flows from the `IdentityServerOptions` lambda and the builder's fluent API.
```csharp
var builder = WebApplication.CreateBuilder(args);
var idsvrBuilder = builder.Services.AddIdentityServer(options =>
{
// Let the issuer be inferred from the request URL (recommended)
// options.IssuerUri = "https://identity.example.com"; // Only set when behind a reverse proxy
// Enable events for diagnostics
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
})
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryClients(Config.Clients);
var app = builder.Build();
app.UseIdentityServer(); // Includes UseAuthentication()
app.UseAuthorization();
app.Run();
```
> **Important:** Call `UseIdentityServer()` instead of `UseAuthentication()` — it registers both the IdentityServer middleware and the authentication middleware.
---
## Pattern 2: Client Definitions
Clients represent applications that request tokens. The three most common configurations are:
### Machine-to-Machine (Client Credentials)
For service-to-service communication with no interactive user:
```csharp
new Client
{
ClientId = "service.worker",
ClientName = "Background Worker Service",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedScopes = { "api1", "api2.read_only" }
}
```
### Interactive Web Application (Authorization Code + PKCE)
For server-rendered web apps that authenticate users and call APIs:
```csharp
new Client
{
ClientId = "web.app",
ClientName = "Web Application",
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true, // Default is true in Duende IS
ClientSecrets = { new Secret("secret".Sha256()) },
// Redirect URIs — must exactly match what the client sends
RedirectUris = { "https://app.example.com/signin-oidc" },
PostLogoutRedirectUris = { "https://app.example.com/signout-callback-oidc" },
FrontChannelLogoutUri = "https://app.example.com/signout-oidc",
// Enable offline access for refresh tokens
AllowOfflineAccess = true,
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
"api1"
}
}
```
### SPA with BFF Pattern
For JavaScript SPAs using the Backend-for-Frontend pattern (see `duende-bff` skill):
```csharp
new Client
{
ClientId = "spa.bff",
ClientName = "SPA with BFF",
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
RequireClientSecret = true, // BFF host holds the secret
ClientSecrets = { new Secret("secret".Sha256()) },
RedirectUris = { "https://app.example.com/signin-oidc" },
PostLogoutRedirectUris = { "https://app.example.com/signout-callback-oidc" },
BackChannelLogoutUri = "https://app.example.com/bff/backchannel",
AllowOfflineAccess = true,
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1"
}
}
```
### Key Client Properties
| Property | Purpose | Default |
|----------|---------|---------|
| `RequirePkce` | Enforce PKCE for authorization code flow | `true` |
| `AllowOfflineAccess` | Enable refresh token issuance | `false` |
| `AccessTokenLifetime` | Access token duration in seconds | `3600` (1 hour) |
| `IdentityTokenLifetime` | Identity token duration in seconds | `300` (5 min) |
| `RefreshTokenUsage` | `ReUse` or `OneTimeOnly` | `ReUse` (recommend `OneTimeOnly` for security) |
| `RefreshTokenExpiration` | `Absolute` or `Sliding` | `Absolute` |
| `AbsoluteRefreshTokenLifetime` | Max refresh token lifetime in seconds | `2592000` (30 days) |
| `AllowedCorsOrigins` | CORS origins for token endpoint calls | empty |
| `RequireConsent` | Show consent screen | `false` |
| `CoordinateLifetimeWithUserSession` | Tie token lifetimes to user session | `false` |
### Defining Clients in appsettings.json
For scenarios where client configuration should be externalized:
```json
{
"IdentityServer": {
"Clients": [
{
"Enabled": true,
"ClientId": "local-dev",
"ClientName": "Local Development",
"ClientSecrets": [
{
"Value": "<Insert Sha256 hash of the secret encoded as Base64 string>"
}
],
"AllowedGrantTypes": ["client_credentials"],
"AllowedScopes": ["api1"]
}
]
}
}
```
```csharp
// Load clients from configuration
idsvrBuilder.AddInMemoryClients(
configuration.GetSection("IdentityServer:Clients"));
```
---
## Pattern 3: Identity Resources
Identity resources define groups of claims about users, requested via the `scope` parameter. They map to claims in the **identity token** and the **userinfo endpoint**.
### Standard Identity Resources
```csharp
public static IEnumerable<IdentityResource> IdentityResources =>
new IdentityResource[]
{
new IdentityResources.OpenId(), // Required — maps to "sub" claim
new IdentityResources.Profile(), // name, family_name, given_name, etc.
new IdentityResources.Email(), // email, email_verified
new IdentityResources.Phone(), // phone_number, phone_number_verified
new IdentityResources.Address(), // address (JSON object)
};
```
### Custom Identity Resources
Define custom identity resources for application-specific user claims:
```csharp
// ✅ Custom identity resource for tenant membership
new IdentityResource(
name: "tenant",
displayName: "Your organization info",
userClaims: new[] { "tenant_id", "tenant_name", "tenant_role" })
{
Required = true // Do not show on consent screen as optional
}
```
> **Key concept:** The `openid` scope is mandatory for any OpenID Connect request. It tells IdentityServer to return the `sub` (subject ID) claim.
---
## Pattern 4: API Scopes and API Resources
API scopes and API resources work together to model your API surface area. Understanding the distinction is critical.
### API Scopes — Permission Model
An `ApiScope` represents a permission or capability a client can request:
```csharp
public static IEnumerable<ApiScope> ApiScopes =>
new ApiScope[]
{
// Simple scope — just a name
new ApiScope("api1", "Main API"),
// Granular scopes for fine-grained access
new ApiScope("catalog.read", "Read product catalog"),
new ApiScope("catalog.write", "Modify product catalog"),
new ApiScope("orders.manage", "Manage orders"),
// Scope that includes specific user claims in the access token
new ApiScope("invoicing", "Invoicing API")
{
UserClaims = { "department", "cost_center" }
}
};
```
### API Resources — Logical API Boundaries
An `ApiResource` represents a logical API (typically a deployed service). It groups scopes and controls the `aud` (audience) claim in access tokens:
```csharp
public static IEnumerable<ApiResource> ApiResources =>
new ApiResource[]
{
new ApiResource("catalog-api", "Product Catalog API")
{
Scopes = { "catalog.read", "catalog.write" },
// These claims are included when any scope in this resource is requested
UserClaims = { "role" }
},
new ApiResource("orders-api", "Order Management API")
{
Scopes = { "orders.manage" },
// API-specific secret for reference token introspection
ApiSecrets = { new Secret("orders-secret".Sha256()) }
}
};
```
### When to Use ApiResource vs ApiScope
| Scenario | Use ApiScope alone? | Add ApiResource? |
|----------|-------------------|------------------|
| Single API, simple permissions | ✅ Sufficient | Optional |
| Multiple APIs sharing a scope | ❌ | ✅ Required for audience isolation |
| Reference token introspection | ❌ | ✅ Required for API secrets |
| Per-API signing algorithms | ❌ | ✅ Use `AllowedTokenSigningAlgorithms` |
| Resource isolation (RFC 8707) | ❌ | ✅ Required |
### Resource Isolation
When multiple APIs share scope names, resource isolation prevents a token issued for one API from being used at another:
```csharp
// Two separate APIs that both have a "read" scope
new ApiResource("inventory-api") { Scopes = { "read", "write" } },
new ApiResource("reporting-api") { Scopes = { "read" } }
```
With resource isolation, the client specifies the target resource in the token request using the `resource` parameter (RFC 8707), and IdentityServer issues a token with a single audience.
---
## Pattern 5: Automatic Key Management
Duende IdentityServer's automatic key management handles signing key creation, rotation, and retirement. This is the recommended approach for production.
```csharp
var idsvrBuilder = builder.Services.AddIdentityServer(options =>
{
// Automatic key management is enabled by default
// Customize rotation policy:
options.KeyManagement.RotationInterval = TimeSpan.FromDays(90); // New key every 90 days
options.KeyManagement.PropagationTime = TimeSpan.FromDays(14); // Announce 14 days early
options.KeyManagement.RetentionDuration = TimeSpan.FromDays(14); // Keep old keys 14 days
options.KeyManagement.DeleteRetiredKeys = true; // Clean up old keys
// Keys are encrypted at rest via ASP.NET Data Protection (default: true)
options.KeyManagement.DataProtectKeys = true;
});
```
### Key Lifecycle
Keys move through these phases:
1. **Announced** — Added to discovery but not used for signing (`PropagationTime` duration)
2. **Active** — Used for signing tokens (until `RotationInterval` is reached)
3. **Retired** — No longer signs tokens, but remains in discovery for validation (`RetentionDuration`)
4. **Deleted** — Removed from discovery (if `DeleteRetiredKeys` is true)
### Multiple Signing Algorithms
Support multiple algorithms for different clients or APIs:
```csharp
options.KeyManagement.SigningAlgorithms = new[]
{
// RS256 for maximum compatibility (first = default)
new SigningAlgorithmOptions(SecurityAlgorithms.RsaSha256)
{
UseX509Certificate = true // Wrap in X.509 certificate
},
// PS256 for enhanced security
new SigningAlgorithmOptions(SecurityAlgorithms.RsaSsaPssSha256),
// ES256 for compact tokens
new SigningAlgorithmOptions(SecurityAlgorithms.EcdsaSha256)
};
```
> The first algorithm in the list becomes the default. Clients and API resources can override via `AllowedTokenSigningAlgorithms`.
### Load-Balanced Deployments
For file-system key storage in load-balanced environments, all instances need access to the same key path:
```csharp
options.KeyManagement.KeyPath = "/home/shared/keys";
```
Alternatively, use the EF Core operational store for database-backed key storage (see `identityserver-stores`).
---
## Pattern 6: Static Key Configuration
When automatic key management is not available or you need explicit control:
```csharp
var idsvrBuilder = builder.Services.AddIdentityServer(options =>
{
options.KeyManagement.Enabled = false;
});
// Load key from secure storage
var signingKey = LoadKeyFromVault(); // Your key loading logic
idsvrBuilder.AddSigningCredential(signingKey, SecurityAlgorithms.RsaSha256);
```
### Manual Key Rotation (Three-Phase Process)
Rotating static keys requires careful sequencing to avoid breaking token validation:
```csharp
// Phase 1: Announce new key (keep signing with old key)
idsvrBuilder.AddSigningCredential(oldKey, SecurityAlgorithms.RsaSha256);
idsvrBuilder.AddValidationKey(newKey, SecurityAlgorithms.RsaSha256);
// Wait for all clients/APIs to refresh their JWKS cache (default: 24h)
// Phase 2: Start signing with new key (keep old key for validation)
idsvrBuilder.AddSigningCredential(newKey, SecurityAlgorithms.RsaSha256);
idsvrBuilder.AddValidationKey(oldKey, SecurityAlgorithms.RsaSha256);
// Wait for all tokens signed with old key to expire
// Phase 3: Remove old key
idsvrBuilder.AddSigningCredential(newKey, SecurityAlgorithms.RsaSha256);
```
---
## Pattern 7: Server-Side Sessions
Server-side sessions store authentication session data in a server-side store instead of the cookie alone. This enables centralized session management, queries, and back-channel logout.
```csharp
var idsvrBuilder = builder.Services.AddIdentityServer(options =>
{
// Remove expired sessions automatically
options.ServerSideSessions.RemoveExpiredSessionsFrequency = TimeSpan.FromMinutes(10);
options.ServerSideSessions.ExpiredSessionsTriggerBackchannelLogout = true;
// Coordinate client token lifetimes with user sessions
options.Authentication.CoordinateClientLifetimesWithUserSession = true;
})
// Server-side sessions are enabled by calling AddServerSideSessions()
.AddServerSideSessions();
```
> **Important:** Server-side sessions are enabled by calling `.AddServerSideSessions()` on the IdentityServer builder — there is no `options.ServerSideSessions.Enabled` property. Add this to the builder chain, not to options.
### Session Expiration Options
| Option | Default | Purpose |
|--------|---------|---------|
| `ServerSideSessions.RemoveExpiredSessionsFrequency` | 10 min | Cleanup interval |
| `ServerSideSessions.RemoveExpiredSessions` | `true` | Enable automatic cleanup |
| `ServerSideSessions.ExpiredSessionsTriggerBackchannelLogout` | `true` | Notify clients on session expiry |
> **Tip:** Combine server-side sessions with `CoordinateClientLifetimesWithUserSession = true` to ensure refresh tokens are revoked when a user's session ends.
---
## Pattern 8: Important IdentityServerOptions
### Events — Enable for Production Monitoring
```csharp
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
```
### Authentication Cookie Settings
```csharp
options.Authentication.CookieLifetime = TimeSpan.FromHours(10);
options.Authentication.CookieSlidingExpiration = false;
```
### Caching (with Store Caching Enabled)
```csharp
options.Caching.ClientStoreExpiration = TimeSpan.FromMinutes(15);
options.Caching.ResourceStoreExpiration = TimeSpan.FromMinutes(15);
```
### Pushed Authorization Requests (PAR)
```csharp
options.PushedAuthorization.Required = true; // Require all clients to use PAR
```
### DPoP (Demonstrating Proof-of-Possession)
```csharp
options.DPoP.ValidationMode = DPoPTokenExpirationValidationMode.Nonce;
options.DPoP.ServerClockSkew = TimeSpan.FromMinutes(5);
```
---
## Common Pitfalls
### 1. Missing openid Scope
```csharp
// ❌ WRONG — OpenID Connect requires the openid scope
new Client
{
AllowedScopes = { "profile", "api1" }
}
// ✅ CORRECT — Always include openid for interactive clients
new Client
{
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1"
}
}
```
### 2. Mismatched Redirect URIs
```csharp
// ❌ WRONG — Trailing slash mismatch causes "invalid_redirect_uri" error
RedirectUris = { "https://app.example.com/signin-oidc/" }
// Client sends: https://app.example.com/signin-oidc (no trailing slash)
// ✅ CORRECT — Exact match required
RedirectUris = { "https://app.example.com/signin-oidc" }
```
### 3. Using ApiScope When ApiResource Is Needed
```csharp
// ❌ WRONG — No audience claim, tokens work at any API
public static IEnumerable<ApiScope> ApiScopes =>
new[] { new ApiScope("read"), new ApiScope("write") };
// ✅ CORRECT — API resource sets audience for token isolation
public static IEnumerable<ApiResource> ApiResources =>
new[]
{
new ApiResource("my-api")
{
Scopes = { "read", "write" }
}
};
```
### 4. Plaintext Client Secrets in Source Control
```csharp
// ❌ WRONG — Secret in source code
ClientSecrets = { new Secret("my-production-secret".Sha256()) }
// ✅ CORRECT — Load from configuration or vault
ClientSecrets = { new Secret(configuration["Clients:Web:Secret"].Sha256()) }
// ✅ ALSO CORRECT — Use asymmetric credentials (no shared secret)
// Client authenticates with a signed JWT assertion
```
### 5. Forgetting AllowOfflineAccess for Refresh Tokens
```csharp
// ❌ Client requests "offline_access" scope but server doesn't allow it
var client = new Client
{
AllowedGrantTypes = GrantTypes.Code,
AllowOfflineAccess = false, // Default
AllowedScopes = { "openid", "api1" }
};
// Client silently won't receive a refresh token
// ✅ Enable offline access explicitly
var client = new Client
{
AllowedGrantTypes = GrantTypes.Code,
AllowOfflineAccess = true,
AllowedScopes = { "openid", "api1" }
};
```
### 6. Not Setting IssuerUri Behind a Reverse Proxy
```csharp
// ❌ IdentityServer behind Nginx but IssuerUri defaults to internal hostname
// Tokens contain iss: "http://internal-host:5000" — clients reject them
// ✅ Set IssuerUri to the external URL
options.IssuerUri = "https://identity.example.com";
```
---
## Production Configuration Checklist
| Setting | Dev | Production |
|---------|-----|------------|
| `KeyManagement.Enabled` | `true` | `true` |
| `KeyManagement.DataProtectKeys` | `true` | `true` + configure Data Protection |
| `Events.Raise*Events` | Optional | All `true` |
| `ServerSideSessions` (`AddServerSideSessions()`) | Optional | Recommended |
| Secrets | In-memory / config | Key vault / certificates |
| Store | In-memory | EF Core or custom |
| HTTPS | Optional | **Required** |
---
## Resources
- [Clients — Duende Docs](https://docs.duendesoftware.com/identityserver/fundamentals/clients/)
- [Resources — Duende Docs](https://docs.duendesoftware.com/identityserver/fundamentals/resources/)
- [Key Management — Duende Docs](https://docs.duendesoftware.com/identityserver/fundamentals/key-management/)
- [IdentityServerOptions Reference — Duende Docs](https://docs.duendesoftware.com/identityserver/reference/options/)
- [Server-Side Sessions — Duende Docs](https://docs.duendesoftware.com/identityserver/ui/server-side-sessions/)
- [Client Model Reference — Duende Docs](https://docs.duendesoftware.com/identityserver/reference/models/client/)
SHA-256: 37bc505627b80c559113248129e836ab46ce8bd05812f155a22cf71349cbafc1