← Duende SkillsCONTENT HISTORY

Update to Duende Skills

Snapshot Sep 30, 2026 · 23:14 UTC · version 0.3.0

Collection source: not recorded for this historical snapshot.

WHAT CHANGED · RULE-BASED ANALYSIS

First saved snapshot

No earlier snapshot is available to establish a change.

Compare saved observations

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

SHA-256 of public snapshot: 5632ce27839eef4ee9fd2ab21a552cba5ed9b75d662477e2f3e1e5e0a8f43aba