{"id":18159,"plugin_id":"plugins_6a86acf7816881918552f3b43bc0db69","kind":"skill","collection_source":null,"comparison_source":null,"observed_at":"2026-09-30T23:14:41.072Z","digest":"ed6d488ba0c76ab10e52a1ce714deea87c9e6b32144b8da1a9f08fc41ffbe9b6","against":null,"payload":{"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"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}