From duende-skills
Configures Dynamic Client Registration for Duende IdentityServer: endpoint setup, authorization policies, custom validation, software statement validation, client store, and separate hosting.
How this skill is triggered — by the user, by Claude, or both
Slash command
/duende-skills:identityserver-dcrThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
- Setting up Dynamic Client Registration (DCR) at `/connect/dcr`
/connect/dcrDynamicClientRegistrationValidatorIClientConfigurationStoreDuende.IdentityServer.Configuration NuGet package/connect/dcr endpoint with an authorization policy — never expose it unauthenticatedDocs: https://docs.duendesoftware.com/identityserver/configuration/dcr
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.
dotnet add package Duende.IdentityServer.Configuration
// 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();
Apply standard ASP.NET Core authorization policies to the DCR endpoint:
// 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");
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": "..."
}
Extend DynamicClientRegistrationValidator to add custom validation logic:
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:
builder.Services.AddIdentityServerConfiguration()
.AddDynamicClientRegistrationValidator<CustomDcrValidator>();
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 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;
}
// Apply claims from software statement to the client
var claims = validationResult.ClaimsIdentity;
context.Client.ClientName = claims.FindFirst("software_name")?.Value;
await base.ValidateSoftwareStatementAsync(context);
}
}
| Interface | Purpose |
|---|---|
IDynamicClientRegistrationRequestProcessor | Process the DCR request (extend default) |
IDynamicClientRegistrationResponseGenerator | Customize the DCR response |
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)
{
// 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
}
}
DCR can be hosted in a separate application from IdentityServer:
// 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();
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.
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).
identityserver-configuration — IdentityServer host configuration, client types, grant types, secret management, and resource configurationidentityserver-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 endpointidentity-security-hardening — Security hardening including HTTPS enforcementnpx claudepluginhub duendesoftware/duende-skills --plugin duende-skillsProvides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.