← Duende SkillsCONTENT HISTORYWHAT CHANGED · RULE-BASED ANALYSIS
Update to Duende Skills
Snapshot Sep 30, 2026 · 23:14 UTC · version 0.3.0
Collection source: not recorded for this historical snapshot.
First saved snapshot
No earlier snapshot is available to establish a change.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"description": "Configuring Dynamic Client Registration (DCR) in Duende IdentityServer: endpoint setup, authorization policies, custom validation with DynamicClientRegistrationValidator, software statement validation, IClientConfigurationStore, and separate DCR hosting.",
"included_files": [],
"name": "identityserver-dcr",
"skill_md_contents": "---\nname: identityserver-dcr\ndescription: \"Configuring Dynamic Client Registration (DCR) in Duende IdentityServer: endpoint setup, authorization policies, custom validation with DynamicClientRegistrationValidator, software statement validation, IClientConfigurationStore, and separate DCR hosting.\"\ninvocable: false\n---\n\n# Dynamic Client Registration (DCR)\n\n## When to Use This Skill\n\n- Setting up Dynamic Client Registration (DCR) at `/connect/dcr`\n- Securing the DCR endpoint with authorization policies\n- Customizing DCR validation with `DynamicClientRegistrationValidator`\n- Implementing software statement validation\n- Persisting dynamically registered clients with `IClientConfigurationStore`\n- Hosting DCR in a separate application from IdentityServer\n\n## Core Principles\n\n- DCR requires the `Duende.IdentityServer.Configuration` NuGet package\n- Requires **Business Edition** or higher license\n- Always secure the `/connect/dcr` endpoint with an authorization policy — never expose it unauthenticated\n- Enforce PKCE and restrict allowed grant types in the DCR validator\n- Use persistent stores (database) for dynamically registered clients in production\n\nDocs: https://docs.duendesoftware.com/identityserver/configuration/dcr\n\n## Overview\n\nDynamic Client Registration allows clients to register themselves at the `/connect/dcr` endpoint per RFC 7591. This feature requires the **Business Edition** or higher and has been available since version 6.3.\n\nDCR uses a separate NuGet package and can be hosted in the same application as IdentityServer or in a separate host.\n\n### Setup\n\n```bash\ndotnet add package Duende.IdentityServer.Configuration\n```\n\n```csharp\n// Program.cs\nbuilder.Services.AddIdentityServer()\n .AddInMemoryClients(Config.Clients)\n .AddInMemoryIdentityResources(Config.IdentityResources)\n .AddInMemoryApiScopes(Config.ApiScopes);\n\nbuilder.Services.AddIdentityServerConfiguration();\n\nvar app = builder.Build();\n\napp.UseIdentityServer();\napp.UseAuthorization();\n\napp.MapDynamicClientRegistration();\n\napp.Run();\n```\n\n`MapDynamicClientRegistration()` is an endpoint-routing extension from the **`Duende.IdentityServer.Configuration`** package (separate from `Duende.IdentityServer`). Call it where you configure the pipeline/endpoint routing — in the quickstart/template hosts this is the `ConfigurePipeline()` method (`HostingExtensions.cs`), alongside `UseIdentityServer()`. `AddIdentityServerConfiguration()` registers the DCR services; `MapDynamicClientRegistration()` maps the `/connect/dcr` endpoint. Both are required.\n\n### Securing the DCR Endpoint\n\nApply standard ASP.NET Core authorization policies to the DCR endpoint:\n\n```csharp\n// Using JWT bearer for the DCR endpoint\nbuilder.Services.AddAuthentication()\n .AddJwtBearer(\"dcr\", options =>\n {\n options.Authority = \"https://identity.example.com\";\n options.Audience = \"IdentityServer.Configuration\";\n options.TokenValidationParameters.ValidTypes = [\"at+jwt\"];\n });\n\nbuilder.Services.AddAuthorization(options =>\n{\n options.AddPolicy(\"dcr\", policy =>\n {\n policy.AddAuthenticationSchemes(\"dcr\");\n policy.RequireAuthenticatedUser();\n policy.RequireClaim(\"scope\", \"IdentityServer.Configuration\");\n });\n});\n\napp.MapDynamicClientRegistration()\n .RequireAuthorization(\"dcr\");\n```\n\n### DCR Request and Response\n\n**Registration request:**\n\n```\nPOST /connect/dcr HTTP/1.1\nContent-Type: application/json\nAuthorization: Bearer <access_token>\n\n{\n \"client_name\": \"My Dynamic App\",\n \"redirect_uris\": [\"https://app.example.com/callback\"],\n \"grant_types\": [\"authorization_code\"],\n \"response_types\": [\"code\"],\n \"token_endpoint_auth_method\": \"client_secret_basic\"\n}\n```\n\n**Registration response:**\n\n```json\n{\n \"client_id\": \"generated-client-id\",\n \"client_secret\": \"generated-secret\",\n \"client_name\": \"My Dynamic App\",\n \"redirect_uris\": [\"https://app.example.com/callback\"],\n \"grant_types\": [\"authorization_code\"],\n \"response_types\": [\"code\"],\n \"registration_client_uri\": \"https://identity.example.com/connect/dcr?client_id=generated-client-id\",\n \"registration_access_token\": \"...\"\n}\n```\n\n### Customizing DCR Validation\n\nExtend `DynamicClientRegistrationValidator` to add custom validation logic:\n\n```csharp\npublic class CustomDcrValidator : DynamicClientRegistrationValidator\n{\n protected override Task ValidateGrantTypesAsync(\n DynamicClientRegistrationContext context)\n {\n // Only allow authorization_code\n var grantTypes = context.Request.GrantTypes;\n if (grantTypes.Any(gt => gt != \"authorization_code\"))\n {\n context.SetError(\"Grant type not allowed\");\n return Task.CompletedTask;\n }\n\n return base.ValidateGrantTypesAsync(context);\n }\n\n protected override Task ValidateRedirectUrisAsync(\n DynamicClientRegistrationContext context)\n {\n // Enforce HTTPS redirect URIs\n var uris = context.Request.RedirectUris;\n if (uris.Any(u => !u.StartsWith(\"https://\", StringComparison.OrdinalIgnoreCase)))\n {\n context.SetError(\"Redirect URIs must use HTTPS\");\n return Task.CompletedTask;\n }\n\n return base.ValidateRedirectUrisAsync(context);\n }\n\n protected override Task SetClientDefaultsAsync(\n DynamicClientRegistrationContext context)\n {\n // Set defaults for dynamically registered clients\n var client = context.Client;\n client.RequirePkce = true;\n client.AllowOfflineAccess = false;\n client.AccessTokenLifetime = 300; // 5 minutes\n\n return base.SetClientDefaultsAsync(context);\n }\n}\n```\n\nRegister:\n\n```csharp\nbuilder.Services.AddIdentityServerConfiguration()\n .AddDynamicClientRegistrationValidator<CustomDcrValidator>();\n```\n\n### DynamicClientRegistrationContext\n\nThe context object passed to validation methods contains:\n\n| Property | Purpose |\n| --------- | ----------------------------------------------------- |\n| `Client` | The IdentityServer `Client` being built |\n| `Request` | The raw DCR request |\n| `Caller` | The `ClaimsPrincipal` of the authenticated DCR caller |\n| `Items` | Dictionary for passing data between validation steps |\n\n### Software Statements\n\nSoftware statements are signed JWTs that contain pre-approved client metadata. Validate them by overriding `ValidateSoftwareStatementAsync`:\n\n```csharp\npublic class SoftwareStatementDcrValidator : DynamicClientRegistrationValidator\n{\n protected override async Task ValidateSoftwareStatementAsync(\n DynamicClientRegistrationContext context)\n {\n var softwareStatement = context.Request.SoftwareStatement;\n if (string.IsNullOrEmpty(softwareStatement))\n {\n context.SetError(\"Software statement required\");\n return;\n }\n\n var handler = new JsonWebTokenHandler();\n var validationResult = await handler.ValidateTokenAsync(\n softwareStatement,\n new TokenValidationParameters\n {\n ValidIssuer = \"https://trusted-authority.example.com\",\n IssuerSigningKeys = await GetTrustedKeysAsync(),\n ValidateLifetime = true\n });\n\n if (!validationResult.IsValid)\n {\n context.SetError(\"Invalid software statement\");\n return;\n }\n\n // Apply claims from software statement to the client\n var claims = validationResult.ClaimsIdentity;\n context.Client.ClientName = claims.FindFirst(\"software_name\")?.Value;\n\n await base.ValidateSoftwareStatementAsync(context);\n }\n}\n```\n\n### Other DCR Extensibility Points\n\n| Interface | Purpose |\n| --------------------------------------------- | ---------------------------------------- |\n| `IDynamicClientRegistrationRequestProcessor` | Process the DCR request (extend default) |\n| `IDynamicClientRegistrationResponseGenerator` | Customize the DCR response |\n\n### Client Configuration Store\n\nDCR needs a persistent store for dynamically registered clients. Use the Entity Framework implementation:\n\n```bash\ndotnet add package Duende.IdentityServer.Configuration.EntityFramework\n```\n\n```csharp\nbuilder.Services.AddIdentityServerConfiguration()\n .AddClientConfigurationStore();\n```\n\nOr implement `IClientConfigurationStore` for a custom backing store:\n\n```csharp\npublic class CustomClientConfigurationStore : IClientConfigurationStore\n{\n public async Task AddAsync(Client client)\n {\n // Persist the dynamically registered client\n }\n\n public async Task<Client?> FindByClientIdAsync(string clientId)\n {\n // Retrieve a dynamically registered client\n }\n\n public async Task UpdateAsync(Client client)\n {\n // Update client configuration\n }\n\n public async Task DeleteAsync(string clientId)\n {\n // Remove a dynamically registered client\n }\n}\n```\n\n### Separate DCR Host\n\nDCR can be hosted in a separate application from IdentityServer:\n\n```csharp\n// Separate DCR host — Program.cs\nbuilder.Services.AddIdentityServerConfiguration(options =>\n{\n options.IdentityServerBaseUrl = \"https://identity.example.com\";\n});\n\nbuilder.Services.AddAuthentication()\n .AddJwtBearer(\"dcr\", options =>\n {\n options.Authority = \"https://identity.example.com\";\n options.Audience = \"IdentityServer.Configuration\";\n });\n\nvar app = builder.Build();\n\napp.UseAuthentication();\napp.UseAuthorization();\napp.MapDynamicClientRegistration().RequireAuthorization(\"dcr\");\n\napp.Run();\n```\n\n## Common Anti-Patterns\n\n- **Exposing the DCR endpoint without authentication** — Always secure `/connect/dcr` with an authorization policy.\n\n- **Allowing dynamically registered clients to use any grant type** — Restrict allowed grant types and enforce PKCE in the DCR validator.\n\n- **Using in-memory stores for DCR clients in production** — Use persistent stores (database) for production deployments.\n\n## Common Pitfalls\n\n1. **Business Edition requirement**: `AddIdentityServerConfiguration()` requires a Business Edition or higher license. Community Edition does not support DCR.\n\n2. **Client secrets**: Dynamically registered clients receive generated secrets. Ensure your `IClientConfigurationStore` stores these securely (hashed, not plaintext).\n\n3. **Software statement trust**: Software statements must be validated against a trusted signing key. Do not accept software statements signed by unknown issuers.\n\n4. **Separate host connectivity**: When hosting DCR separately, it must be able to communicate with IdentityServer's data stores. Ensure the `IClientConfigurationStore` is backed by the same database that IdentityServer reads from (or uses a shared data layer).\n\n## Related Skills\n\n- `identityserver-configuration` — IdentityServer host configuration, client types, grant types, secret management, and resource configuration\n- `identityserver-saml` — SAML 2.0 Identity Provider (the other advanced IdentityServer feature)\n- `identityserver-stores` — Persistent store patterns (useful for custom `IClientConfigurationStore`)\n- `aspnetcore-authorization` — Authorization policies for securing the DCR endpoint\n- `identity-security-hardening` — Security hardening including HTTPS enforcement\n"
}SHA-256 of public snapshot: ed6d488ba0c76ab10e52a1ce714deea87c9e6b32144b8da1a9f08fc41ffbe9b6