From duende-skills
Configures Duende IdentityServer: client definitions, API/identity resources, scopes, signing keys, and server-side sessions. Covers grant types, secret management, and in-memory or database-backed config.
How this skill is triggered — by the user, by Claude, or both
Slash command
/duende-skills:identityserver-configurationThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Use this skill when:
Use this skill when:
IdentityServerOptions for production deploymentsGrantTypes.Code for all interactive clients. Never use implicit flow for new applications.ApiResource to control the aud claim and isolate API boundaries. Use ApiScope for fine-grained permission modeling within those boundaries.identityserver-stores — EF Core persistence for configuration and operational dataoauth-oidc-protocols — Protocol fundamentals that underpin these configuration choicesidentity-security-hardening — Production hardening of IdentityServer deploymentstoken-management — Client-side token lifecycle with Duende.AccessTokenManagementaspnetcore-authentication — Configuring OIDC authentication in client applicationsDocs: https://docs.duendesoftware.com/identityserver/configuration
Load these sub-documents when the user's question specifically targets one of these areas:
| Document | Description | When to Load |
|---|---|---|
| docs/client-types.md | Grant type selection matrix, client property reference tables, client authentication methods (shared secret, private_key_jwt, mTLS), secret rollover, and CORS | private_key_jwt, mTLS, secret rotation, refresh token settings, client authentication, CORS origins |
| docs/resources-scopes.md | Resource type decision matrix, identity resources, API scopes (including parameterized scopes), and API resources with audience isolation | aud claim, audience isolation, parameterized scopes, EmitStaticAudienceClaim, API Resources, Identity Resources |
Register Duende IdentityServer in Program.cs with AddIdentityServer. All configuration flows from the IdentityServerOptions lambda and the builder's fluent API.
var builder = WebApplication.CreateBuilder(args);
var idsvrBuilder = builder.Services.AddIdentityServer(options =>
{
// Let the issuer be inferred from the request URL (recommended)
// options.IssuerUri = "https://identity.example.com"; // Only set when behind a reverse proxy
// Enable events for diagnostics
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
})
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryClients(Config.Clients);
var app = builder.Build();
app.UseIdentityServer(); // Includes UseAuthentication()
app.UseAuthorization();
app.Run();
Important: Call
UseIdentityServer()instead ofUseAuthentication()— it registers both the IdentityServer middleware and the authentication middleware.
Clients represent applications that request tokens. The three most common configurations are:
For service-to-service communication with no interactive user:
new Client
{
ClientId = "service.worker",
ClientName = "Background Worker Service",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedScopes = { "api1", "api2.read_only" }
}
For server-rendered web apps that authenticate users and call APIs:
new Client
{
ClientId = "web.app",
ClientName = "Web Application",
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true, // Default is true in Duende IS
ClientSecrets = { new Secret("secret".Sha256()) },
// Redirect URIs — must exactly match what the client sends
RedirectUris = { "https://app.example.com/signin-oidc" },
PostLogoutRedirectUris = { "https://app.example.com/signout-callback-oidc" },
FrontChannelLogoutUri = "https://app.example.com/signout-oidc",
// Enable offline access for refresh tokens
AllowOfflineAccess = true,
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
"api1"
}
}
For JavaScript SPAs using the Backend-for-Frontend pattern (see duende-bff skill):
new Client
{
ClientId = "spa.bff",
ClientName = "SPA with BFF",
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
RequireClientSecret = true, // BFF host holds the secret
ClientSecrets = { new Secret("secret".Sha256()) },
RedirectUris = { "https://app.example.com/signin-oidc" },
PostLogoutRedirectUris = { "https://app.example.com/signout-callback-oidc" },
BackChannelLogoutUri = "https://app.example.com/bff/backchannel",
AllowOfflineAccess = true,
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1"
}
}
| Property | Purpose | Default |
|---|---|---|
RequirePkce | Enforce PKCE for authorization code flow | true |
AllowOfflineAccess | Enable refresh token issuance | false |
AccessTokenLifetime | Access token duration in seconds | 3600 (1 hour) |
IdentityTokenLifetime | Identity token duration in seconds | 300 (5 min) |
RefreshTokenUsage | ReUse or OneTimeOnly | ReUse (recommend OneTimeOnly for security) |
RefreshTokenExpiration | Absolute or Sliding | Absolute |
AbsoluteRefreshTokenLifetime | Max refresh token lifetime in seconds | 2592000 (30 days) |
AllowedCorsOrigins | CORS origins for token endpoint calls | empty |
RequireConsent | Show consent screen | false |
CoordinateLifetimeWithUserSession | Tie token lifetimes to user session | false |
For scenarios where client configuration should be externalized:
{
"IdentityServer": {
"Clients": [
{
"Enabled": true,
"ClientId": "local-dev",
"ClientName": "Local Development",
"ClientSecrets": [
{
"Value": "<Insert Sha256 hash of the secret encoded as Base64 string>"
}
],
"AllowedGrantTypes": ["client_credentials"],
"AllowedScopes": ["api1"]
}
]
}
}
// Load clients from configuration
idsvrBuilder.AddInMemoryClients(
configuration.GetSection("IdentityServer:Clients"));
Identity resources define groups of claims about users, requested via the scope parameter. They map to claims in the identity token and the userinfo endpoint.
public static IEnumerable<IdentityResource> IdentityResources =>
new IdentityResource[]
{
new IdentityResources.OpenId(), // Required — maps to "sub" claim
new IdentityResources.Profile(), // name, family_name, given_name, etc.
new IdentityResources.Email(), // email, email_verified
new IdentityResources.Phone(), // phone_number, phone_number_verified
new IdentityResources.Address(), // address (JSON object)
};
Define custom identity resources for application-specific user claims:
// ✅ Custom identity resource for tenant membership
new IdentityResource(
name: "tenant",
displayName: "Your organization info",
userClaims: new[] { "tenant_id", "tenant_name", "tenant_role" })
{
Required = true // Do not show on consent screen as optional
}
Key concept: The
openidscope is mandatory for any OpenID Connect request. It tells IdentityServer to return thesub(subject ID) claim.
API scopes and API resources work together to model your API surface area. Understanding the distinction is critical.
An ApiScope represents a permission or capability a client can request:
public static IEnumerable<ApiScope> ApiScopes =>
new ApiScope[]
{
// Simple scope — just a name
new ApiScope("api1", "Main API"),
// Granular scopes for fine-grained access
new ApiScope("catalog.read", "Read product catalog"),
new ApiScope("catalog.write", "Modify product catalog"),
new ApiScope("orders.manage", "Manage orders"),
// Scope that includes specific user claims in the access token
new ApiScope("invoicing", "Invoicing API")
{
UserClaims = { "department", "cost_center" }
}
};
An ApiResource represents a logical API (typically a deployed service). It groups scopes and controls the aud (audience) claim in access tokens:
public static IEnumerable<ApiResource> ApiResources =>
new ApiResource[]
{
new ApiResource("catalog-api", "Product Catalog API")
{
Scopes = { "catalog.read", "catalog.write" },
// These claims are included when any scope in this resource is requested
UserClaims = { "role" }
},
new ApiResource("orders-api", "Order Management API")
{
Scopes = { "orders.manage" },
// API-specific secret for reference token introspection
ApiSecrets = { new Secret("orders-secret".Sha256()) }
}
};
| Scenario | Use ApiScope alone? | Add ApiResource? |
|---|---|---|
| Single API, simple permissions | ✅ Sufficient | Optional |
| Multiple APIs sharing a scope | ❌ | ✅ Required for audience isolation |
| Reference token introspection | ❌ | ✅ Required for API secrets |
| Per-API signing algorithms | ❌ | ✅ Use AllowedTokenSigningAlgorithms |
| Resource isolation (RFC 8707) | ❌ | ✅ Required |
When multiple APIs share scope names, resource isolation prevents a token issued for one API from being used at another:
// Two separate APIs that both have a "read" scope
new ApiResource("inventory-api") { Scopes = { "read", "write" } },
new ApiResource("reporting-api") { Scopes = { "read" } }
With resource isolation, the client specifies the target resource in the token request using the resource parameter (RFC 8707), and IdentityServer issues a token with a single audience.
Duende IdentityServer's automatic key management handles signing key creation, rotation, and retirement. This is the recommended approach for production.
var idsvrBuilder = builder.Services.AddIdentityServer(options =>
{
// Automatic key management is enabled by default
// Customize rotation policy:
options.KeyManagement.RotationInterval = TimeSpan.FromDays(90); // New key every 90 days
options.KeyManagement.PropagationTime = TimeSpan.FromDays(14); // Announce 14 days early
options.KeyManagement.RetentionDuration = TimeSpan.FromDays(14); // Keep old keys 14 days
options.KeyManagement.DeleteRetiredKeys = true; // Clean up old keys
// Keys are encrypted at rest via ASP.NET Data Protection (default: true)
options.KeyManagement.DataProtectKeys = true;
});
Keys move through these phases:
PropagationTime duration)RotationInterval is reached)RetentionDuration)DeleteRetiredKeys is true)Support multiple algorithms for different clients or APIs:
options.KeyManagement.SigningAlgorithms = new[]
{
// RS256 for maximum compatibility (first = default)
new SigningAlgorithmOptions(SecurityAlgorithms.RsaSha256)
{
UseX509Certificate = true // Wrap in X.509 certificate
},
// PS256 for enhanced security
new SigningAlgorithmOptions(SecurityAlgorithms.RsaSsaPssSha256),
// ES256 for compact tokens
new SigningAlgorithmOptions(SecurityAlgorithms.EcdsaSha256)
};
The first algorithm in the list becomes the default. Clients and API resources can override via
AllowedTokenSigningAlgorithms.
For file-system key storage in load-balanced environments, all instances need access to the same key path:
options.KeyManagement.KeyPath = "/home/shared/keys";
Alternatively, use the EF Core operational store for database-backed key storage (see identityserver-stores).
When automatic key management is not available or you need explicit control:
var idsvrBuilder = builder.Services.AddIdentityServer(options =>
{
options.KeyManagement.Enabled = false;
});
// Load key from secure storage
var signingKey = LoadKeyFromVault(); // Your key loading logic
idsvrBuilder.AddSigningCredential(signingKey, SecurityAlgorithms.RsaSha256);
Rotating static keys requires careful sequencing to avoid breaking token validation:
// Phase 1: Announce new key (keep signing with old key)
idsvrBuilder.AddSigningCredential(oldKey, SecurityAlgorithms.RsaSha256);
idsvrBuilder.AddValidationKey(newKey, SecurityAlgorithms.RsaSha256);
// Wait for all clients/APIs to refresh their JWKS cache (default: 24h)
// Phase 2: Start signing with new key (keep old key for validation)
idsvrBuilder.AddSigningCredential(newKey, SecurityAlgorithms.RsaSha256);
idsvrBuilder.AddValidationKey(oldKey, SecurityAlgorithms.RsaSha256);
// Wait for all tokens signed with old key to expire
// Phase 3: Remove old key
idsvrBuilder.AddSigningCredential(newKey, SecurityAlgorithms.RsaSha256);
Server-side sessions store authentication session data in a server-side store instead of the cookie alone. This enables centralized session management, queries, and back-channel logout.
var idsvrBuilder = builder.Services.AddIdentityServer(options =>
{
// Remove expired sessions automatically
options.ServerSideSessions.RemoveExpiredSessionsFrequency = TimeSpan.FromMinutes(10);
options.ServerSideSessions.ExpiredSessionsTriggerBackchannelLogout = true;
// Coordinate client token lifetimes with user sessions
options.Authentication.CoordinateClientLifetimesWithUserSession = true;
})
// Server-side sessions are enabled by calling AddServerSideSessions()
.AddServerSideSessions();
Important: Server-side sessions are enabled by calling
.AddServerSideSessions()on the IdentityServer builder — there is nooptions.ServerSideSessions.Enabledproperty. Add this to the builder chain, not to options.
| Option | Default | Purpose |
|---|---|---|
ServerSideSessions.RemoveExpiredSessionsFrequency | 10 min | Cleanup interval |
ServerSideSessions.RemoveExpiredSessions | true | Enable automatic cleanup |
ServerSideSessions.ExpiredSessionsTriggerBackchannelLogout | true | Notify clients on session expiry |
Tip: Combine server-side sessions with
CoordinateClientLifetimesWithUserSession = trueto ensure refresh tokens are revoked when a user's session ends.
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
options.Authentication.CookieLifetime = TimeSpan.FromHours(10);
options.Authentication.CookieSlidingExpiration = false;
options.Caching.ClientStoreExpiration = TimeSpan.FromMinutes(15);
options.Caching.ResourceStoreExpiration = TimeSpan.FromMinutes(15);
options.PushedAuthorization.Required = true; // Require all clients to use PAR
options.DPoP.ValidationMode = DPoPTokenExpirationValidationMode.Nonce;
options.DPoP.ServerClockSkew = TimeSpan.FromMinutes(5);
// ❌ WRONG — OpenID Connect requires the openid scope
new Client
{
AllowedScopes = { "profile", "api1" }
}
// ✅ CORRECT — Always include openid for interactive clients
new Client
{
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1"
}
}
// ❌ WRONG — Trailing slash mismatch causes "invalid_redirect_uri" error
RedirectUris = { "https://app.example.com/signin-oidc/" }
// Client sends: https://app.example.com/signin-oidc (no trailing slash)
// ✅ CORRECT — Exact match required
RedirectUris = { "https://app.example.com/signin-oidc" }
// ❌ WRONG — No audience claim, tokens work at any API
public static IEnumerable<ApiScope> ApiScopes =>
new[] { new ApiScope("read"), new ApiScope("write") };
// ✅ CORRECT — API resource sets audience for token isolation
public static IEnumerable<ApiResource> ApiResources =>
new[]
{
new ApiResource("my-api")
{
Scopes = { "read", "write" }
}
};
// ❌ WRONG — Secret in source code
ClientSecrets = { new Secret("my-production-secret".Sha256()) }
// ✅ CORRECT — Load from configuration or vault
ClientSecrets = { new Secret(configuration["Clients:Web:Secret"].Sha256()) }
// ✅ ALSO CORRECT — Use asymmetric credentials (no shared secret)
// Client authenticates with a signed JWT assertion
// ❌ Client requests "offline_access" scope but server doesn't allow it
var client = new Client
{
AllowedGrantTypes = GrantTypes.Code,
AllowOfflineAccess = false, // Default
AllowedScopes = { "openid", "api1" }
};
// Client silently won't receive a refresh token
// ✅ Enable offline access explicitly
var client = new Client
{
AllowedGrantTypes = GrantTypes.Code,
AllowOfflineAccess = true,
AllowedScopes = { "openid", "api1" }
};
// ❌ IdentityServer behind Nginx but IssuerUri defaults to internal hostname
// Tokens contain iss: "http://internal-host:5000" — clients reject them
// ✅ Set IssuerUri to the external URL
options.IssuerUri = "https://identity.example.com";
| Setting | Dev | Production |
|---|---|---|
KeyManagement.Enabled | true | true |
KeyManagement.DataProtectKeys | true | true + configure Data Protection |
Events.Raise*Events | Optional | All true |
ServerSideSessions (AddServerSideSessions()) | Optional | Recommended |
| Secrets | In-memory / config | Key vault / certificates |
| Store | In-memory | EF Core or custom |
| HTTPS | Optional | Required |
npx 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.