Skip to content
New Livestream: How Banks Protect Their Apps with FAPI 2.0. Register Now!

Configuration Admin APIs

AddStorage registers administration services alongside the stores that IdentityServer uses at runtime. These are .NET service APIs, not preconfigured HTTP endpoints. You decide how to expose them through a protected administration application, command-line tool or deployment process.

Never expose an administration API without authentication and authorization. Keep it on a trusted network where possible, require a dedicated administrative policy, validate anti-forgery protections for browser clients and audit changes.

ServiceManages
IClientAdminOpenID Connect and OAuth clients, including client secrets
IApiScopeAdminAPI scopes
IApiResourceAdminAPI resources, scope relationships and API secrets
IIdentityResourceAdminIdentity resources
IIdentityProviderAdminDynamic identity providers
ISamlServiceProviderAdminSAML service providers

Each service supports create, get, update, query and delete operations. Resolve the service from a dependency injection scope, such as an authorized endpoint or a scoped application service.

The following examples show the minimum shape of each configuration type. Create referenced scopes before resources or clients that use them.

ConfigurationAdmin.cs
using Duende.IdentityServer.Admin;
using Duende.IdentityServer.Admin.ApiScopes;
static Task<SaveResult<Guid>> CreateApiScope(
IApiScopeAdmin admin,
CancellationToken ct) =>
admin.CreateAsync(
new ApiScopeConfiguration
{
Name = "inventory.read",
DisplayName = "Read inventory"
},
ct);
ConfigurationAdmin.cs
using Duende.IdentityServer.Admin;
using Duende.IdentityServer.Admin.ApiResources;
static Task<SaveResult<Guid>> CreateApiResource(
IApiResourceAdmin admin,
CancellationToken ct) =>
admin.CreateAsync(
new ApiResourceConfiguration
{
Name = "inventory",
DisplayName = "Inventory API",
Scopes = ["inventory.read"]
},
ct);
ConfigurationAdmin.cs
using Duende.IdentityServer.Admin;
using Duende.IdentityServer.Admin.IdentityResources;
static Task<SaveResult<Guid>> CreateIdentityResource(
IIdentityResourceAdmin admin,
CancellationToken ct) =>
admin.CreateAsync(
new IdentityResourceConfiguration
{
Name = "employee_profile",
DisplayName = "Employee profile",
UserClaims = ["department", "employee_id"]
},
ct);
ConfigurationAdmin.cs
using Duende.IdentityServer.Admin;
using Duende.IdentityServer.Admin.Clients;
static Task<SaveResult<Guid>> CreateClient(
IClientAdmin admin,
CancellationToken ct) =>
admin.CreateAsync(
new CreateClient
{
ClientId = "inventory-worker",
ClientName = "Inventory worker",
AllowedGrantTypes = ["client_credentials"],
AllowedScopes = ["inventory.read"],
ClientSecrets =
[
new CreateClientSecret
{
// Replace this with a secret from a secure source,
// such as Azure Key Vault.
PlaintextValue = "replace-with-a-secret-from-a-secure-source"
}
]
},
ct);

The API hashes plaintext client and API secret values before storage. Read secrets from a secret manager, never source control or application configuration committed with your code. Secret values are not returned by read operations.

ConfigurationAdmin.cs
using Duende.IdentityServer.Admin;
using Duende.IdentityServer.Admin.IdentityProviders;
static Task<SaveResult<Guid>> CreateIdentityProvider(
IIdentityProviderAdmin admin,
string clientSecret,
CancellationToken ct) =>
admin.CreateAsync(
new IdentityProviderConfiguration
{
Scheme = "corporate-oidc",
DisplayName = "Corporate sign-in",
Type = "oidc",
Properties = new Dictionary<string, string>
{
["Authority"] = "https://login.example.com",
["ClientId"] = "identityserver",
["ClientSecret"] = clientSecret
}
},
ct);

Register the corresponding dynamic provider type and supply all protocol-specific properties it requires. Read clientSecret from a secure source, such as Azure Key Vault. IdentityServer validates the configuration before storing it.

ConfigurationAdmin.cs
using Duende.IdentityServer.Admin;
using Duende.IdentityServer.Admin.SamlServiceProviders;
using Duende.IdentityServer.Models;
static Task<SaveResult<Guid>> CreateSamlServiceProvider(
ISamlServiceProviderAdmin admin,
CancellationToken ct) =>
admin.CreateAsync(
new SamlServiceProviderConfiguration
{
EntityId = "https://service-provider.example.com",
DisplayName = "Example service provider",
AssertionConsumerServiceUrls =
[
new SamlIndexedEndpointConfiguration
{
Location = "https://service-provider.example.com/saml/acs",
Binding = SamlBinding.HttpPost,
Index = 0,
IsDefault = true
}
]
},
ct);

Create, update and delete operations return SaveResult<TId>. Check IsSuccess before using Id or Version; failed results contain structured Errors. Get operations return GetResult<T>, whose Found property tells you whether Item and Version are available.

Updates require the version returned by the preceding create or get operation:

ConfigurationAdmin.cs
// ID returned by a previous CreateAsync or QueryAsync call.
var scopeId = new Guid("0198f3a4-5760-7000-8000-000000000001");
var current = await apiScopes.GetAsync(scopeId, ct);
if (!current.Found)
{
throw new InvalidOperationException("The API scope does not exist.");
}
current.Item.DisplayName = "Inventory read access";
var updated = await apiScopes.UpdateAsync(
scopeId,
current.Item,
current.Version,
ct);
if (!updated.IsSuccess)
{
throw new InvalidOperationException(updated.Errors.ToString());
}

This optimistic concurrency check prevents one administrator from silently overwriting another administrator’s changes. Handle validation, duplicate-name, not-found and version-conflict errors explicitly in your administration interface.

Use QueryAsync to list entities. The result contains the current page and pagination metadata:

ConfigurationAdmin.cs
using Duende.IdentityServer.Admin.ApiScopes;
using Duende.Storage.Querying;
var request = QueryRequest.Create<ApiScopeFilter, ApiScopeSortField>(
new ApiScopeFilter { Name = "inventory" });
var page = await apiScopes.QueryAsync(request, ct);
foreach (var scope in page.Items)
{
Console.WriteLine($"{scope.Id}: {scope.Name}");
}

Filters can use contains matching, so compare a returned business identifier exactly when you need one specific entity. Use the continuation tokens on QueryResult<T> to retrieve additional pages.

Delete an entity by its storage ID:

ConfigurationAdmin.cs
var deleted = await apiScopes.DeleteAsync(scopeId, ct);
if (!deleted.IsSuccess)
{
throw new InvalidOperationException(deleted.Errors.ToString());
}

For custom fields on clients and resources, configure data extension schemas.