← Files Duende SkillsARCHIVED FILE
skills/identityserver-dcr/SKILL.md
11.2 KB · Oct 2, 2026 · 00:31 UTC
---
name: identityserver-dcr
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."
invocable: false
---
# Dynamic Client Registration (DCR)
## When to Use This Skill
- Setting up Dynamic Client Registration (DCR) at `/connect/dcr`
- Securing the DCR endpoint with authorization policies
- Customizing DCR validation with `DynamicClientRegistrationValidator`
- Implementing software statement validation
- Persisting dynamically registered clients with `IClientConfigurationStore`
- Hosting DCR in a separate application from IdentityServer
## Core Principles
- DCR requires the `Duende.IdentityServer.Configuration` NuGet package
- Requires **Business Edition** or higher license
- Always secure the `/connect/dcr` endpoint with an authorization policy — never expose it unauthenticated
- Enforce PKCE and restrict allowed grant types in the DCR validator
- Use persistent stores (database) for dynamically registered clients in production
Docs: https://docs.duendesoftware.com/identityserver/configuration/dcr
## Overview
Dynamic 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.
DCR uses a separate NuGet package and can be hosted in the same application as IdentityServer or in a separate host.
### Setup
```bash
dotnet add package Duende.IdentityServer.Configuration
```
```csharp
// Program.cs
builder.Services.AddIdentityServer()
.AddInMemoryClients(Config.Clients)
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryApiScopes(Config.ApiScopes);
builder.Services.AddIdentityServerConfiguration();
var app = builder.Build();
app.UseIdentityServer();
app.UseAuthorization();
app.MapDynamicClientRegistration();
app.Run();
```
`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.
### Securing the DCR Endpoint
Apply standard ASP.NET Core authorization policies to the DCR endpoint:
```csharp
// Using JWT bearer for the DCR endpoint
builder.Services.AddAuthentication()
.AddJwtBearer("dcr", options =>
{
options.Authority = "https://identity.example.com";
options.Audience = "IdentityServer.Configuration";
options.TokenValidationParameters.ValidTypes = ["at+jwt"];
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("dcr", policy =>
{
policy.AddAuthenticationSchemes("dcr");
policy.RequireAuthenticatedUser();
policy.RequireClaim("scope", "IdentityServer.Configuration");
});
});
app.MapDynamicClientRegistration()
.RequireAuthorization("dcr");
```
### DCR Request and Response
**Registration request:**
```
POST /connect/dcr HTTP/1.1
Content-Type: application/json
Authorization: Bearer <access_token>
{
"client_name": "My Dynamic App",
"redirect_uris": ["https://app.example.com/callback"],
"grant_types": ["authorization_code"],
"response_types": ["code"],
"token_endpoint_auth_method": "client_secret_basic"
}
```
**Registration response:**
```json
{
"client_id": "generated-client-id",
"client_secret": "generated-secret",
"client_name": "My Dynamic App",
"redirect_uris": ["https://app.example.com/callback"],
"grant_types": ["authorization_code"],
"response_types": ["code"],
"registration_client_uri": "https://identity.example.com/connect/dcr?client_id=generated-client-id",
"registration_access_token": "..."
}
```
### Customizing DCR Validation
Extend `DynamicClientRegistrationValidator` to add custom validation logic:
```csharp
public class CustomDcrValidator : DynamicClientRegistrationValidator
{
protected override Task ValidateGrantTypesAsync(
DynamicClientRegistrationContext context)
{
// Only allow authorization_code
var grantTypes = context.Request.GrantTypes;
if (grantTypes.Any(gt => gt != "authorization_code"))
{
context.SetError("Grant type not allowed");
return Task.CompletedTask;
}
return base.ValidateGrantTypesAsync(context);
}
protected override Task ValidateRedirectUrisAsync(
DynamicClientRegistrationContext context)
{
// Enforce HTTPS redirect URIs
var uris = context.Request.RedirectUris;
if (uris.Any(u => !u.StartsWith("https://", StringComparison.OrdinalIgnoreCase)))
{
context.SetError("Redirect URIs must use HTTPS");
return Task.CompletedTask;
}
return base.ValidateRedirectUrisAsync(context);
}
protected override Task SetClientDefaultsAsync(
DynamicClientRegistrationContext context)
{
// Set defaults for dynamically registered clients
var client = context.Client;
client.RequirePkce = true;
client.AllowOfflineAccess = false;
client.AccessTokenLifetime = 300; // 5 minutes
return base.SetClientDefaultsAsync(context);
}
}
```
Register:
```csharp
builder.Services.AddIdentityServerConfiguration()
.AddDynamicClientRegistrationValidator<CustomDcrValidator>();
```
### DynamicClientRegistrationContext
The context object passed to validation methods contains:
| Property | Purpose |
| --------- | ----------------------------------------------------- |
| `Client` | The IdentityServer `Client` being built |
| `Request` | The raw DCR request |
| `Caller` | The `ClaimsPrincipal` of the authenticated DCR caller |
| `Items` | Dictionary for passing data between validation steps |
### Software Statements
Software statements are signed JWTs that contain pre-approved client metadata. Validate them by overriding `ValidateSoftwareStatementAsync`:
```csharp
public class SoftwareStatementDcrValidator : DynamicClientRegistrationValidator
{
protected override async Task ValidateSoftwareStatementAsync(
DynamicClientRegistrationContext context)
{
var softwareStatement = context.Request.SoftwareStatement;
if (string.IsNullOrEmpty(softwareStatement))
{
context.SetError("Software statement required");
return;
}
var handler = new JsonWebTokenHandler();
var validationResult = await handler.ValidateTokenAsync(
softwareStatement,
new TokenValidationParameters
{
ValidIssuer = "https://trusted-authority.example.com",
IssuerSigningKeys = await GetTrustedKeysAsync(),
ValidateLifetime = true
});
if (!validationResult.IsValid)
{
context.SetError("Invalid software statement");
return;
}
// Apply claims from software statement to the client
var claims = validationResult.ClaimsIdentity;
context.Client.ClientName = claims.FindFirst("software_name")?.Value;
await base.ValidateSoftwareStatementAsync(context);
}
}
```
### Other DCR Extensibility Points
| Interface | Purpose |
| --------------------------------------------- | ---------------------------------------- |
| `IDynamicClientRegistrationRequestProcessor` | Process the DCR request (extend default) |
| `IDynamicClientRegistrationResponseGenerator` | Customize the DCR response |
### Client Configuration Store
DCR needs a persistent store for dynamically registered clients. Use the Entity Framework implementation:
```bash
dotnet add package Duende.IdentityServer.Configuration.EntityFramework
```
```csharp
builder.Services.AddIdentityServerConfiguration()
.AddClientConfigurationStore();
```
Or implement `IClientConfigurationStore` for a custom backing store:
```csharp
public class CustomClientConfigurationStore : IClientConfigurationStore
{
public async Task AddAsync(Client client)
{
// Persist the dynamically registered client
}
public async Task<Client?> FindByClientIdAsync(string clientId)
{
// Retrieve a dynamically registered client
}
public async Task UpdateAsync(Client client)
{
// Update client configuration
}
public async Task DeleteAsync(string clientId)
{
// Remove a dynamically registered client
}
}
```
### Separate DCR Host
DCR can be hosted in a separate application from IdentityServer:
```csharp
// Separate DCR host — Program.cs
builder.Services.AddIdentityServerConfiguration(options =>
{
options.IdentityServerBaseUrl = "https://identity.example.com";
});
builder.Services.AddAuthentication()
.AddJwtBearer("dcr", options =>
{
options.Authority = "https://identity.example.com";
options.Audience = "IdentityServer.Configuration";
});
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapDynamicClientRegistration().RequireAuthorization("dcr");
app.Run();
```
## Common Anti-Patterns
- **Exposing the DCR endpoint without authentication** — Always secure `/connect/dcr` with an authorization policy.
- **Allowing dynamically registered clients to use any grant type** — Restrict allowed grant types and enforce PKCE in the DCR validator.
- **Using in-memory stores for DCR clients in production** — Use persistent stores (database) for production deployments.
## Common Pitfalls
1. **Business Edition requirement**: `AddIdentityServerConfiguration()` requires a Business Edition or higher license. Community Edition does not support DCR.
2. **Client secrets**: Dynamically registered clients receive generated secrets. Ensure your `IClientConfigurationStore` stores these securely (hashed, not plaintext).
3. **Software statement trust**: Software statements must be validated against a trusted signing key. Do not accept software statements signed by unknown issuers.
4. **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).
## Related Skills
- `identityserver-configuration` — IdentityServer host configuration, client types, grant types, secret management, and resource configuration
- `identityserver-saml` — SAML 2.0 Identity Provider (the other advanced IdentityServer feature)
- `identityserver-stores` — Persistent store patterns (useful for custom `IClientConfigurationStore`)
- `aspnetcore-authorization` — Authorization policies for securing the DCR endpoint
- `identity-security-hardening` — Security hardening including HTTPS enforcement
SHA-256: be8ff57c6b8cafcbf62a23960dbd3a0941a916e4454ecaf189dc243584341d59