From duende-skills
Migrates IdentityServer4 projects to Duende IdentityServer v8, covering package replacement, namespace changes, EF Core schema migration, .NET upgrade, license config, and signing key migration.
How this skill is triggered — by the user, by Claude, or both
Slash command
/duende-skills:identityserver4-migrationThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> **Scope: IdentityServer4 only.** This skill covers migrating from **IdentityServer4** (v3.x and v4.x) to Duende IdentityServer. It does **not** cover migrating from **IdentityServer3** (the older Thinktecture/`IdentityServer3` NuGet package that ran on OWIN/Katana and .NET Framework). IdentityServer3 is a fundamentally different product with a different API surface, configuration model, and h...
Scope: IdentityServer4 only. This skill covers migrating from IdentityServer4 (v3.x and v4.x) to Duende IdentityServer. It does not cover migrating from IdentityServer3 (the older Thinktecture/
IdentityServer3NuGet package that ran on OWIN/Katana and .NET Framework). IdentityServer3 is a fundamentally different product with a different API surface, configuration model, and hosting stack. If you are on IdentityServer3, you must first port to IdentityServer4 on ASP.NET Core before using this guide.
IdentityServer4.* namespaces to Duende.IdentityServer.*netcoreapp3.1 or net5.0 to a current LTS versionStartup.cs/Program.cs hosting patterns from Generic Host to minimal hostingMigration has two stages if starting from v3.x. IdentityServer4 v3 → v4 introduced breaking changes in the ApiResource/ApiScope relationship (parent-child to many-to-many). If you are on v3, first migrate to v4 semantics, then migrate from v4 to Duende. If you are already on IdentityServer4 v4.x, you can go directly to Duende.
Duende IdentityServer is the direct successor to IdentityServer4. The API surface is intentionally similar — most code changes are namespace and package renames. Behavioral changes are minimal, but the database schema has new tables and columns for features like automatic key management, server-side sessions, DPoP, and PAR.
The .NET target framework must be upgraded alongside the IdentityServer migration. IdentityServer4 ran on netcoreapp3.1 or net5.0. Duende IdentityServer v8 requires net10.0. You must follow Microsoft's ASP.NET Core migration guides for each major version jump.
Database migrations require careful handling to avoid data loss. The v3 → v4 schema change renames tables and restructures relationships. A naive EF Core migration will drop and recreate tables, losing data. Use the provided delta SQL scripts or manually craft migrations that preserve data.
Licensing is required for production use. Duende IdentityServer requires a valid license key for production. Without one, it runs in community/trial mode and logs a warning on startup.
Docs: https://docs.duendesoftware.com/identityserver/upgrades
IdentityServer4 v3.x or v4.x
│
▼ (Step 0: Run Migration Analysis Tool against running instance)
│
▼ (Stage 1: v3 → v4 API changes + DB migration — skip if already on v4)
IdentityServer4 v4.x
│
▼ (Stage 2: packages + namespaces + .NET upgrade + DB migration)
Duende IdentityServer v8.x
If already on IdentityServer4 v4.x, skip directly to Stage 2.
Before making any code changes, run the Migration Analysis Tool against your running IdentityServer4 instance. This tool inspects your live configuration and produces a report with specific recommendations.
The tool is a single file, MigrationAnalysisController.cs, that you drop into your existing IdentityServer4 project. It does not require additional NuGet packages.
| Data Point | Why It Matters |
|---|---|
| .NET runtime version | Flags if you need to upgrade to .NET 10 |
| IdentityServer4 version | Determines if Stage 1 (v3 → v4) is needed before proceeding |
| Client inventory | Counts interactive (authorization code) vs. non-interactive (client credentials) clients — this determines which Duende license edition you need |
| Issuer URI | Reports the configured IssuerUri — must be preserved in Duende to avoid breaking existing tokens and client trust relationships |
| Signing credential store type | Identifies custom signing stores that may need compatibility updates |
| Signing credential key ID | Records the current key ID for signing key migration planning |
| Data protection application name | Flags missing or path-based discriminators that will break after .NET upgrade |
| Data protection repository type | Warns if keys are stored ephemerally (lost on restart) instead of a persistent store |
| Authentication schemes | Lists all registered authentication handlers — third-party handlers (non-Microsoft, non-IdentityServer4) may need version updates for the new ASP.NET Core version |
MigrationAnalysisController.cs and add it to your IdentityServer4 projectIndex() method — the default placeholder checks for username "scott" which you must replace with your own authorization logic/MigrationAnalysis while authenticatedThe tool loads clients from in-memory configuration or EF Core stores automatically. If you use a custom client store, you will need to modify the constructor to wire up your client retrieval.
Note: Duende also offers a free IdentityServer4 upgrade assessment to walk through your upgrade path.
Skip this section if you are already on IdentityServer4 v4.x.
<!-- Old (v3) -->
<PackageReference Include="IdentityServer4" Version="3.1.4" />
<PackageReference Include="IdentityServer4.EntityFramework" Version="3.1.4" />
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="3.1.4" />
<!-- New (v4) -->
<PackageReference Include="IdentityServer4" Version="4.1.2" />
<PackageReference Include="IdentityServer4.EntityFramework" Version="4.1.2" />
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="4.1.2" />
In v3, ApiScope was a child of ApiResource. In v4, scopes are independent top-level objects with a many-to-many relationship to API resources. You must register them separately:
// v3: Scopes nested inside ApiResource
new ApiResource("api1", "My API")
{
Scopes = { new Scope("api1.read"), new Scope("api1.write") }
}
// v4: Scopes are independent; ApiResource references scope names
public static IEnumerable<ApiScope> ApiScopes => new[]
{
new ApiScope("api1.read", "Read access to API 1"),
new ApiScope("api1.write", "Write access to API 1")
};
public static IEnumerable<ApiResource> ApiResources => new[]
{
new ApiResource("api1", "My API")
{
Scopes = { "api1.read", "api1.write" } // string references, not Scope objects
}
};
// Register both:
services.AddIdentityServer()
.AddInMemoryApiScopes(Config.ApiScopes) // NEW in v4
.AddInMemoryApiResources(Config.ApiResources)
.AddInMemoryClients(Config.Clients);
HttpContext.SignInAsync signature change:
// v3
await HttpContext.SignInAsync(user.SubjectId, user.Username, props);
// v4
var isuser = new IdentityServerUser(user.SubjectId)
{
DisplayName = user.Username
};
await HttpContext.SignInAsync(isuser, props);
AuthorizationRequest property changes:
// v3
var clientId = request.ClientId;
var scopes = request.ScopesRequested;
var isPkce = await _clientStore.IsPkceClientAsync(context.ClientId);
// v4
var clientId = request.Client.ClientId;
var scopes = request.ValidatedResources.RawScopeValues;
var isPkce = context.IsNativeClient();
Consent response changes:
// v3
var grantedConsent = new ConsentResponse
{
ScopesConsented = consentedScopes
};
// v4
var grantedConsent = new ConsentResponse
{
ScopesValuesConsented = consentedScopes // renamed property
};
Grant management method renames:
// v3
await _interaction.GetAllUserConsentsAsync();
// v4
await _interaction.GetAllUserGrantsAsync();
External provider callback consolidation:
// v3: separate methods per protocol
ProcessLoginCallbackForOidc();
ProcessLoginCallbackForWsFed();
ProcessLoginCallbackForSaml2p();
// v4: single unified method
ProcessLoginCallback();
PersistedGrantDbContext — Standard EF migration:
dotnet ef migrations add Grants_v4 -c PersistedGrantDbContext -o Migrations/PersistedGrantDb
dotnet ef database update -c PersistedGrantDbContext
New columns added: ConsumedTime, Description, SessionId on PersistedGrants and DeviceCodes.
ConfigurationDbContext — Requires custom SQL to preserve data:
The v3 → v4 schema change renames tables:
ApiClaims → ApiResourceClaimsApiProperties → ApiResourcePropertiesApiSecrets → ApiResourceSecretsIdentityClaims → IdentityResourceClaimsIdentityProperties → IdentityResourcePropertiesAnd restructures the ApiScopes relationship (scopes become independent, linked via ApiResourceScopes join table).
Do not rely on auto-generated EF migrations for this step — they will drop and recreate tables, losing data. Instead:
Create the migration scaffold:
dotnet ef migrations add Config_v4 -c ConfigurationDbContext -o Migrations/ConfigurationDb
Embed a custom delta SQL script that migrates data before dropping old tables:
-- Move data from old tables to new tables
INSERT INTO ApiResourceClaims (Id, [Type], ApiResourceId)
SELECT Id, [Type], ApiResourceId FROM ApiClaims;
INSERT INTO ApiResourceProperties (Id, [Key], [Value], ApiResourceId)
SELECT Id, [Key], [Value], ApiResourceId FROM ApiProperties;
INSERT INTO ApiResourceSecrets (Id, [Description], [Value], [Expiration], [Type], [Created], ApiResourceId)
SELECT Id, [Description], [Value], [Expiration], [Type], [Created], ApiResourceId FROM ApiSecrets;
INSERT INTO IdentityResourceClaims (Id, [Type], IdentityResourceId)
SELECT Id, [Type], IdentityResourceId FROM IdentityClaims;
INSERT INTO IdentityResourceProperties (Id, [Key], [Value], IdentityResourceId)
SELECT Id, [Key], [Value], IdentityResourceId FROM IdentityProperties;
-- Migrate scope-resource relationship to join table
INSERT INTO ApiResourceScopes ([Scope], [ApiResourceId])
SELECT [Name], [ApiResourceId] FROM ApiScopes;
-- Remove old foreign key column from ApiScopes
-- (handled by EF migration after data is moved)
Modify the generated migration to execute the SQL script before the destructive operations.
Apply: dotnet ef database update -c ConfigurationDbContext
Reference implementation: UpgradeSample-IdentityServer4-v3
Update from netcoreapp3.1 or net5.0 to net10.0 (required by Duende IdentityServer v8):
<!-- Old -->
<TargetFramework>netcoreapp3.1</TargetFramework>
<!-- New -->
<TargetFramework>net10.0</TargetFramework>
Follow the Microsoft ASP.NET Core migration guides for each major version jump. Key changes include:
WebApplication.CreateBuilder replaces Startup.cs + Program.cs pattern)ImplicitUsings enabled by defaultMicrosoft.EntityFrameworkCore.* packages to match the .NET version<!-- Old (IdentityServer4) -->
<PackageReference Include="IdentityServer4" Version="4.1.2" />
<PackageReference Include="IdentityServer4.EntityFramework" Version="4.1.2" />
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="4.1.2" />
<PackageReference Include="IdentityModel" Version="5.2.0" />
<!-- New (Duende) -->
<PackageReference Include="Duende.IdentityServer" Version="8.0.0" />
<PackageReference Include="Duende.IdentityServer.EntityFramework" Version="8.0.0" />
<PackageReference Include="Duende.IdentityServer.AspNetIdentity" Version="8.0.0" />
<PackageReference Include="Duende.IdentityModel" Version="8.0.0" />
Also update EF Core and other ASP.NET Core packages to match the new target framework:
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0" />
Search and replace all IdentityServer4 namespaces with Duende.IdentityServer:
// Old
using IdentityServer4;
using IdentityServer4.Models;
using IdentityServer4.Services;
using IdentityServer4.Stores;
using IdentityServer4.Extensions;
using IdentityServer4.Events;
using IdentityServer4.Test;
using IdentityServer4.Validation;
using IdentityServer4.EntityFramework.DbContexts;
using IdentityServer4.EntityFramework.Mappers;
using IdentityServer4.EntityFramework.Options;
using IdentityModel;
// New
using Duende.IdentityServer;
using Duende.IdentityServer.Models;
using Duende.IdentityServer.Services;
using Duende.IdentityServer.Stores;
using Duende.IdentityServer.Extensions;
using Duende.IdentityServer.Events;
using Duende.IdentityServer.Test;
using Duende.IdentityServer.Validation;
using Duende.IdentityServer.EntityFramework.DbContexts;
using Duende.IdentityServer.EntityFramework.Mappers;
using Duende.IdentityServer.EntityFramework.Options;
using Duende.IdentityModel;
Also update any fully-qualified type references in code and configuration files.
If migrating from netcoreapp3.1, convert the Startup.cs + Program.cs pattern to minimal hosting:
// Old: Startup.cs + Program.cs pattern
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentityServer()
.AddConfigurationStore(options => { /* ... */ })
.AddOperationalStore(options => { /* ... */ });
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(e => e.MapDefaultControllerRoute());
}
}
// New: Minimal hosting in Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddIdentityServer()
.AddConfigurationStore(options => { /* ... */ })
.AddOperationalStore(options => { /* ... */ });
var app = builder.Build();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthorization();
app.MapDefaultControllerRoute();
app.Run();
The issuer URI (iss claim) must remain identical after migration. If it changes, all existing tokens become invalid and client trust relationships break.
// If you had an explicit IssuerUri in IdentityServer4, keep it:
builder.Services.AddIdentityServer(options =>
{
options.IssuerUri = "https://identity.example.com";
});
// If the issuer was inferred from the request URL in IS4 (no explicit IssuerUri set),
// verify that the Duende host uses the same URL/port/scheme.
If your IS4 instance inferred the issuer from the request (no explicit IssuerUri configured), check the /.well-known/openid-configuration of your old instance, note the issuer value, and explicitly set it in the Duende configuration to be safe.
Add the license key configuration — required for production:
builder.Services.AddIdentityServer(options =>
{
options.LicenseKey = builder.Configuration["IdentityServer:LicenseKey"];
});
Store the license key in a secret manager, environment variable, or key vault — never in source-controlled appsettings.json.
Without a license key, IdentityServer runs in community/trial mode and logs a warning on startup. This is acceptable for local development.
Choosing the right edition: The license edition depends on your client inventory. Count interactive clients (those using authorization_code grant type — typically web apps, SPAs, native apps) vs. non-interactive clients (those using client_credentials — typically machine-to-machine). Run the Migration Analysis Tool (Step 0) to get these counts automatically. See Duende IdentityServer Pricing for edition thresholds.
IdentityServer4 projects commonly used AddDeveloperSigningCredential() for development signing keys. Duende IdentityServer includes automatic key management (Business/Enterprise editions):
// Old (remove)
services.AddIdentityServer()
.AddDeveloperSigningCredential();
// New: Automatic key management is built-in (Business/Enterprise)
// No explicit call needed — keys are created and rotated automatically
// Or for Community edition, configure a static signing credential:
builder.Services.AddIdentityServer()
.AddSigningCredential(new X509Certificate2("signing.pfx", "password"));
Create EF Core migrations for both contexts:
dotnet ef migrations add UpdateToDuende_v8 -c PersistedGrantDbContext \
-o Data/Migrations/IdentityServer/PersistedGrantDb
dotnet ef migrations add UpdateToDuende_v8 -c ConfigurationDbContext \
-o Data/Migrations/IdentityServer/ConfigurationDb
Apply:
dotnet ef database update -c PersistedGrantDbContext
dotnet ef database update -c ConfigurationDbContext
New tables and columns in Duende IdentityServer v8:
| Context | Change | Purpose |
|---|---|---|
| Operational | Keys table (new) | Automatic key management storage |
| Operational | ServerSideSessions table (new) | Server-side session management |
| Operational | PushedAuthorizationRequests table (new) | PAR support |
| Operational | SamlSignInStates table (new) | SAML SSO state |
| Operational | SamlLogoutSessions table (new) | SAML SLO session tracking |
| Operational | ConsumedTime index on PersistedGrants | Performance optimization |
| Configuration | IdentityProviders table (new) | Dynamic OIDC provider configuration |
| Configuration | SamlServiceProviders table (new) | SAML SP registration |
| Configuration | RequireResourceIndicator column on ApiResources | Resource indicator support |
| Configuration | Timestamp columns on entities | Created, updated, last accessed tracking |
| Configuration | Unique constraints on child tables | Prevent duplicate entries |
| Client | InitiateLoginUri | Third-party initiated login |
| Client | RequireDPoP, DPoPValidationMode, DPoPClockSkew | DPoP enforcement |
| Client | RequirePushedAuthorization, PushedAuthorizationLifetime | PAR requirement |
Note on redirect URI column length: The RedirectUri column length was reduced from 2000 to 400 characters. This is safe unless you use redirect URIs longer than 400 characters, which is extremely uncommon.
Set an explicit application name to prevent data protection key invalidation when paths change between .NET versions. See ASP.NET Core Data Protection for comprehensive guidance — this is a cross-cutting concern for all Duende SDKs.
builder.Services.AddDataProtection()
.PersistKeysToDbContext<DataProtectionKeyContext>()
.SetApplicationName("YourIdentityServer");
Why this matters: The default application name (content root path) changed between .NET versions:
If you relied on the default, tokens encrypted before the .NET upgrade will not decrypt after it.
Persistent key storage is required in production. If data protection has no explicit repository configured (PersistKeysToDbContext, PersistKeysToFileSystem, PersistKeysToAzureBlobStorage, etc.), keys are stored in-memory and lost on restart — meaning all encrypted data (persisted grants, cookies, antiforgery tokens) becomes unreadable. The Migration Analysis Tool (Step 0) flags this as (not set) in the data protection repository type check. If you see this, add persistent key storage before migrating.
Decision tree for signing key migration:
Can you restart all client applications and APIs? → Remove old key, use automatic key management. All clients will fetch the new key from the discovery document.
Cannot restart everything? → Export the old signing key and configure it alongside automatic key management so existing tokens remain valid during the transition period.
// Transitional: keep old key while automatic key management creates new keys
builder.Services.AddIdentityServer()
.AddSigningCredential(existingRsaKey) // old key for validation
// automatic key management handles new token signing
Third-party authentication handlers registered in your IdentityServer4 project may need updates for the target ASP.NET Core version. The Migration Analysis Tool (Step 0) lists all registered authentication schemes and flags non-Microsoft, non-IdentityServer4 handlers.
Common handlers that need updates:
| Old Handler | Action |
|---|---|
WS-Federation (Microsoft.AspNetCore.Authentication.WsFederation) | Update NuGet package to match target .NET version |
| SAML2P (e.g., Sustainsys.Saml2, ITfoxtec.Identity.Saml2) | Update to a version compatible with .NET 10; note that Duende v8 has built-in SAML 2.0 IdP support (see identityserver-saml skill) |
| Social providers (Google, Facebook, Twitter, etc.) | Update Microsoft.AspNetCore.Authentication.* packages to match target framework |
Custom IAuthenticationHandler implementations | Verify interface compatibility — AuthenticateAsync, ChallengeAsync, ForbidAsync signatures are stable, but constructor-injected types may have changed |
After migration, verify all external login flows work end-to-end. Missing or incompatible handlers will cause runtime errors when users attempt to authenticate via those schemes.
Not all IdentityServer4 projects have a UI layer. Projects that only configure stores, clients, and resources (e.g., headless API-only hosts or database migration utilities) have no UI to migrate — skip this step entirely.
If your project includes the IdentityServer4 Quickstart UI (login, logout, consent, error pages — typically MVC controllers with Razor views under Views/ and Controllers/, or Razor Pages under Pages/), those templates must be updated. The IdentityServer4 Quickstart UI and the Duende IdentityServer UI templates have diverged significantly since 2018.
Pages/) instead of MVC controllers (Controllers/ + Views/). Your existing MVC-based UI will still compile and work after the namespace update, but you will miss newer UI flows.IIdentityServerInteractionService must be updated for v4 API changes (e.g., request.ClientId → request.Client.ClientId, ScopesConsented → ScopesValuesConsented, GetAllUserConsentsAsync → GetAllUserGrantsAsync).@using IdentityServer4 directives in .cshtml files must become @using Duende.IdentityServer. Check _ViewImports.cshtml and individual view files.Preferred: Start fresh with Duende templates and port your customizations:
dotnet new install Duende.Templates
dotnet new duende-is-ui
This scaffolds the current Duende UI pages into your project. Diff the output against your existing UI to identify where your customizations belong.
Alternative: Incremental — use a diff tool to compare your current UI with the Duende templates and apply changes surgically. This is practical when your UI has heavy customizations and starting fresh would lose too much work.
Minimum viable update (for projects that just need to compile): Update namespaces in all .cshtml files and _ViewImports.cshtml, fix v4 API changes in controllers/page models, and defer the full UI refresh. This gets you running but leaves you on the older layout.
Use this checklist to track your migration progress:
net10.0IdentityServer4.* → Duende.IdentityServer.*IdentityServer4 → Duende.IdentityServer; IdentityModel → Duende.IdentityModelnetcoreapp3.1)IssuerUri explicitly to match existing deploymentAddDeveloperSigningCredential — use automatic key management or static credentialConfigurationDbContext and PersistedGrantDbContextSetApplicationName and persistent key storage/.well-known/openid-configurationIdentityServer4 namespace not found after package update — You replaced the NuGet package but didn't update namespaces. Search and replace using IdentityServer4 with using Duende.IdentityServer across all files.
Scope type not found in v4 — In v4, Scope was removed as a nested type. API scopes are now top-level ApiScope objects. Update ApiResource.Scopes from Scope objects to string scope names.
EF migration drops and recreates tables (v3 → v4) — The auto-generated migration will destroy data. Use the custom delta SQL script approach described in Step 1.4.
Data protection keys invalid after .NET upgrade — The default application discriminator changed between .NET versions. Set SetApplicationName() explicitly to maintain key continuity.
Data protection keys lost on restart — If no persistent key repository is configured, data protection uses an ephemeral in-memory store. All encrypted data (persisted grants, cookies) becomes unreadable after restart. Configure PersistKeysToDbContext, PersistKeysToFileSystem, or PersistKeysToAzureBlobStorage.
Issuer URI changed after migration — If the iss claim in tokens no longer matches what clients/APIs expect, all existing tokens and trust relationships break. Set options.IssuerUri explicitly to match the value from your old /.well-known/openid-configuration.
Third-party authentication handler fails at runtime — External auth handlers (WS-Fed, SAML2P, social providers) compiled against older ASP.NET Core versions may fail to load. Update their NuGet packages to versions compatible with your target .NET version.
Discovery document shows HTTP instead of HTTPS — If behind a reverse proxy, configure forwarded headers. This is not migration-specific but commonly surfaces during deployment changes.
AddDeveloperSigningCredential method not found — This method still exists in Duende but is intended for development only. For production, use automatic key management or a static signing credential.
IsPkceClientAsync method not found — This was removed in v4. Use context.IsNativeClient() or check request.Client.RequirePkce directly.
ConsentResponse.ScopesConsented property not found — Renamed to ScopesValuesConsented in v4.
Existing persisted grants fail to decrypt after migration — Ensure ASP.NET Core Data Protection keys from the old deployment are still available. Data protection encrypts the Data column in persisted grants. If keys are lost, stored grants become unreadable.
| IdentityServer Version | .NET Version | EF Core Version |
|---|---|---|
| IdentityServer4 v3.x | .NET Core 3.1 | EF Core 3.1 |
| IdentityServer4 v4.x | .NET Core 3.1 / .NET 5 | EF Core 3.1 / 5.0 |
| Duende IdentityServer v5.x | .NET 5 / .NET 6 | EF Core 5.0 / 6.0 |
| Duende IdentityServer v6.x | .NET 6 / .NET 7 | EF Core 6.0 / 7.0 |
| Duende IdentityServer v7.x | .NET 8 | EF Core 8.0 |
| Duende IdentityServer v8.x | .NET 10 | EF Core 10.0 |
identityserver-hosting-setup — setting up and hosting Duende IdentityServeridentityserver-stores — EF Core store configuration and migrationsidentityserver-configuration — client and resource configurationidentityserver-key-management — signing key management and rotationidentityserver-upgrade-v7-to-v8 — additional v8 breaking changes (HybridCache, TimeProvider, CancellationToken on all interfaces)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.