| 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
dotnet add package Duende.IdentityServer.Configuration
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();
Securing the DCR Endpoint
Apply standard ASP.NET Core authorization policies to 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:
{
"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:
public class CustomDcrValidator : DynamicClientRegistrationValidator
{
protected override Task ValidateGrantTypesAsync(
DynamicClientRegistrationContext context)
{
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)
{
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)
{
var client = context.Client;
client.RequirePkce = true;
client.AllowOfflineAccess = false;
client.AccessTokenLifetime = 300;
return base.SetClientDefaultsAsync(context);
}
}
Register:
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:
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;
}
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:
dotnet add package Duende.IdentityServer.Configuration.EntityFramework
builder.Services.AddIdentityServerConfiguration()
.AddClientConfigurationStore();
Or implement IClientConfigurationStore for a custom backing store:
public class CustomClientConfigurationStore : IClientConfigurationStore
{
public async Task AddAsync(Client client)
{
}
public async Task<Client?> FindByClientIdAsync(string clientId)
{
}
public async Task UpdateAsync(Client client)
{
}
public async Task DeleteAsync(string clientId)
{
}
}
Separate DCR Host
DCR can be hosted in a separate application from IdentityServer:
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
-
Business Edition requirement: AddIdentityServerConfiguration() requires a Business Edition or higher license. Community Edition does not support DCR.
-
Client secrets: Dynamically registered clients receive generated secrets. Ensure your IClientConfigurationStore stores these securely (hashed, not plaintext).
-
Software statement trust: Software statements must be validated against a trusted signing key. Do not accept software statements signed by unknown issuers.
-
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