Skip to content
Back to skills

Identityserver Configuration

ASecurity

Configure Duende IdentityServer including client definitions, API resources, identity resources, scopes, signing credentials, and server-side sessions. Covers client types (M2M, interactive, SPA), grant types, API Scopes vs API Resources vs Identity Resources, secret management, and client authentication methods. Includes both in-memory and database-backed configuration.

  • 9 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 16, 2026
developmentjavascriptgojavaapidatabasefrontendbackendsecurity

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 3 files and shows the line behind each finding

Scanned October 16, 2026

npx -y skills add DuendeSoftware/duende-skills --skill identityserver-configuration --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Identityserver Configuration?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Identityserver Configuration
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/duendesoftware-identityserver-configuration/badge)](https://www.skillsdirectory.com/skills/duendesoftware-identityserver-configuration)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: identityserver-configuration
description: Configure Duende IdentityServer including client definitions, API resources, identity resources, scopes, signing credentials, and server-side sessions. Covers client types (M2M, interactive, SPA), grant types, API Scopes vs API Resources vs Identity Resources, secret management, and client authentication methods. Includes both in-memory and database-backed configuration.
invocable: false
---

# Duende IdentityServer Configuration

## When to Use This Skill

Use this skill when:
- Setting up a new Duende IdentityServer host
- Defining or modifying client registrations
- Configuring API resources, API scopes, or identity resources
- Setting up signing key management (automatic or static)
- Enabling server-side sessions
- Tuning `IdentityServerOptions` for production deployments
- Migrating from IdentityServer4 to Duende IdentityServer

## Core Principles

1. **Authorization Code + PKCE by Default** — Use `GrantTypes.Code` for all interactive clients. Never use implicit flow for new applications.
2. **Least Privilege Scopes** — Grant clients only the scopes they need. Avoid wildcard or overly broad scope assignments.
3. **Automatic Key Management** — Prefer the built-in automatic key rotation over static key configuration in production.
4. **API Resources for Audience Isolation** — Use `ApiResource` to control the `aud` claim and isolate API boundaries. Use `ApiScope` for fine-grained permission modeling within those boundaries.
5. **Server-Side Sessions for Enterprise** — Enable server-side sessions when you need centralized session management, back-channel logout, or session queries.

## Related Skills

- `identityserver-duende-storage` — opt-in Duende Storage provider (preview), admin APIs for clients/resources
- `identityserver-spaces` — multi-tenancy with isolated data per space (preview)

- `identityserver-stores` — EF Core persistence for configuration and operational data
- `oauth-oidc-protocols` — Protocol fundamentals that underpin these configuration choices
- `identity-security-hardening` — Production hardening of IdentityServer deployments
- `token-management` — Client-side token lifecycle with Duende.AccessTokenManagement
- `aspnetcore-authentication` — Configuring OIDC authentication in client applications

Docs: https://docs.duendesoftware.com/identityserver/configuration

---

## Sub-Documents

Load these sub-documents when the user's question specifically targets one of these areas:

| Document | Description | When to Load |
|----------|-------------|--------------|
| [docs/client-types.md](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](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, IScopeParser, IResourceValidator, EmitStaticAudienceClaim, API Resources, Identity Resources |

---

## Pattern 1: Hosting and Basic Setup

Register Duende IdentityServer in `Program.cs` with `AddIdentityServer`. All configuration flows from the `IdentityServerOptions` lambda and the builder's fluent API.

```csharp
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 of `UseAuthentication()` — it registers both the IdentityServer middleware and the authentication middleware.

---

## Pattern 2: Client Definitions

Clients represent applications that request tokens. The three most common configurations are:

### Machine-to-Machine (Client Credentials)

For service-to-service communication with no interactive user:

```csharp
new Client
{
    ClientId = "service.worker",
    ClientName = "Background Worker Service",

    AllowedGrantTypes = GrantTypes.ClientCredentials,
    ClientSecrets = { new Secret("secret".Sha256()) },

    AllowedScopes = { "api1", "api2.read_only" }
}
```

### Interactive Web Application (Authorization Code + PKCE)

For server-rendered web apps that authenticate users and call APIs:

```csharp
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"
    }
}
```

### SPA with BFF Pattern

For JavaScript SPAs using the Backend-for-Frontend pattern (see `duende-bff` skill):

```csharp
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"
    }
}
```

### Key Client Properties

| 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` |

### Defining Clients in appsettings.json

For scenarios where client configuration should be externalized:

```json
{
  "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"]
      }
    ]
  }
}
```

```csharp
// Load clients from configuration
idsvrBuilder.AddInMemoryClients(
    configuration.GetSection("IdentityServer:Clients"));
```

---

## Pattern 3: Identity Resources

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**.

### Standard Identity Resources

```csharp
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)
    };
```

### Custom Identity Resources

Define custom identity resources for application-specific user claims:

```csharp
// ✅ 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 `openid` scope is mandatory for any OpenID Connect request. It tells IdentityServer to return the `sub` (subject ID) claim.

---

## Pattern 4: API Scopes and API Resources

API scopes and API resources work together to model your API surface area. Understanding the distinction is critical.

### API Scopes — Permission Model

An `ApiScope` represents a permission or capability a client can request:

```csharp
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" }
        }
    };
```

### API Resources — Logical API Boundaries

An `ApiResource` represents a logical API (typically a deployed service). It groups scopes and controls the `aud` (audience) claim in access tokens:

```csharp
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()) }
        }
    };
```

### When to Use ApiResource vs ApiScope

| 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 |

### Resource Isolation

When multiple APIs share scope names, resource isolation prevents a token issued for one API from being used at another:

```csharp
// 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.

---

## Pattern 5: Automatic Key Management

Duende IdentityServer's automatic key management handles signing key creation, rotation, and retirement. This is the recommended approach for production.

```csharp
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;
});
```

### Key Lifecycle

Keys move through these phases:
1. **Announced** — Added to discovery but not used for signing (`PropagationTime` duration)
2. **Active** — Used for signing tokens (until `RotationInterval` is reached)
3. **Retired** — No longer signs tokens, but remains in discovery for validation (`RetentionDuration`)
4. **Deleted** — Removed from discovery (if `DeleteRetiredKeys` is true)

### Multiple Signing Algorithms

Support multiple algorithms for different clients or APIs:

```csharp
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`.

### Load-Balanced Deployments

For file-system key storage in load-balanced environments, all instances need access to the same key path:

```csharp
options.KeyManagement.KeyPath = "/home/shared/keys";
```

Alternatively, use the EF Core operational store for database-backed key storage (see `identityserver-stores`).

---

## Pattern 6: Static Key Configuration

When automatic key management is not available or you need explicit control:

```csharp
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);
```

### Manual Key Rotation (Three-Phase Process)

Rotating static keys requires careful sequencing to avoid breaking token validation:

```csharp
// 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);
```

---

## Pattern 7: Server-Side Sessions

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.

```csharp
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 no `options.ServerSideSessions.Enabled` property. Add this to the builder chain, not to options.

### Session Expiration Options

| Option | Default | Purpose |
|--------|---------|---------|
| `ServerSideSessions.RemoveExpiredSessionsFrequency` | 10 min | Cleanup interval |
| `ServerSideSessions.RemoveExpiredSessions` | `true` | Enable automatic cleanup |
| `ServerSideSessions.ExpiredSessionsTriggerBackchannelLogout` | `false` | Notify all session clients on expiry (clients with coordinated token lifetime are always notified) |

> **Tip:** Combine server-side sessions with `CoordinateClientLifetimesWithUserSession = true` to ensure refresh tokens are revoked when a user's session ends.

---

## Pattern 8: Important IdentityServerOptions

### Events — Enable for Production Monitoring

```csharp
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
```

### Authentication Cookie Settings

```csharp
options.Authentication.CookieLifetime = TimeSpan.FromHours(10);
options.Authentication.CookieSlidingExpiration = false;
```

### Caching (with Store Caching Enabled)

```csharp
options.Caching.ClientStoreExpiration = TimeSpan.FromMinutes(15);
options.Caching.ResourceStoreExpiration = TimeSpan.FromMinutes(15);
```

### Pushed Authorization Requests (PAR)

```csharp
options.PushedAuthorization.Required = true; // Require all clients to use PAR
```

### DPoP (Demonstrating Proof-of-Possession)

```csharp
options.DPoP.ValidationMode = DPoPTokenExpirationValidationMode.Nonce;
options.DPoP.ServerClockSkew = TimeSpan.FromMinutes(5);
```

---

## Common Pitfalls

### 1. Missing openid Scope

```csharp
// ❌ 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"
    }
}
```

### 2. Mismatched Redirect URIs

```csharp
// ❌ 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" }
```

### 3. Using ApiScope When ApiResource Is Needed

```csharp
// ❌ 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" }
        }
    };
```

### 4. Plaintext Client Secrets in Source Control

```csharp
// ❌ 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
```

### 5. Forgetting AllowOfflineAccess for Refresh Tokens

```csharp
// ❌ 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" }
};
```

### 6. Not Setting IssuerUri Behind a Reverse Proxy

```csharp
// ❌ IdentityServer behind Nginx but IssuerUri defaults to internal hostname
// Tokens contain iss: "http://internal-host:5000" — clients reject them

// ✅ Preferred: forward the public host (ForwardedHeaders) so the issuer is inferred correctly
// ✅ Or, with a single issuer, set IssuerUri to the external URL
options.IssuerUri = "https://identity.example.com";
```

> **Exception — Multi-Issuer and Spaces:** leave `IssuerUri` **unset**. IdentityServer infers the issuer from each request's origin/path base, giving each domain or [space](../identityserver-spaces/SKILL.md) its own `iss`. A fixed `IssuerUri` makes every domain/space return the same issuer (and disables Multi-Issuer). Multi-Issuer changes only `iss`; for isolated per-tenant configuration and operational data use Spaces (`identityserver-spaces`, preview, requires Duende Storage). Other tenant option: `ValidateTenantOnAuthorization` (default `false`) compares the `tenant:` acr value with the session's `tenant` claim (see `identityserver-ui-flows`).

---

## Production Configuration Checklist

| 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** |

---

## Resources

- [Clients — Duende Docs](https://docs.duendesoftware.com/identityserver/fundamentals/clients/)
- [Resources — Duende Docs](https://docs.duendesoftware.com/identityserver/fundamentals/resources/)
- [Key Management — Duende Docs](https://docs.duendesoftware.com/identityserver/fundamentals/key-management/)
- [IdentityServerOptions Reference — Duende Docs](https://docs.duendesoftware.com/identityserver/reference/options/)
- [Server-Side Sessions — Duende Docs](https://docs.duendesoftware.com/identityserver/ui/server-side-sessions/)
- [Client Model Reference — Duende Docs](https://docs.duendesoftware.com/identityserver/reference/models/client/)

Files in this skill

  • SKILL.md22.3 KB
  • docs/client-types.md8.1 KB
  • docs/resources-scopes.md10 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.