From duende-skills
Migrates Duende IdentityServer from v7.4 to v8.0: updates TFM to .NET 10, replaces deprecated APIs (ICache→HybridCache, IClock→TimeProvider), adds CancellationToken parameters, and runs EF Core migrations for new SAML tables.
How this skill is triggered — by the user, by Claude, or both
Slash command
/duende-skills:identityserver-upgrade-v7-to-v8The summary Claude sees in its skill listing — used to decide when to auto-load this skill
- Upgrading a Duende IdentityServer project from v7.4 to v8.0
8.0.1; use whatever the latest stable (non-prerelease) 8.x version is at the time of the upgrade.Docs: https://docs.duendesoftware.com/identityserver/upgrades/v7_4-to-v8_0/
<!-- ❌ Before -->
<TargetFramework>net8.0</TargetFramework>
<!-- ✅ After -->
<TargetFramework>net10.0</TargetFramework>
Check NuGet for the latest stable 8.x version. At time of writing, that is 8.0.1, but use whatever is current:
<PackageReference Include="Duende.IdentityServer" Version="8.0.1" />
<PackageReference Include="Duende.IdentityServer.EntityFramework" Version="8.0.1" />
<!-- Update all Duende.* packages to the latest stable 8.x version -->
Two migrations are required — one for the Configuration Store and one for the Operational Store:
# Configuration Store — adds 7 SAML-related tables
dotnet ef migrations add Update_DuendeIdentityServer_v8_0 \
-c ConfigurationDbContext -o Migrations/ConfigurationDb
dotnet ef database update -c ConfigurationDbContext
# Operational Store — adds 3 SAML session tables
dotnet ef migrations add Update_DuendeIdentityServer_v8_0_Saml \
-c PersistedGrantDbContext -o Migrations/PersistedGrantDb
dotnet ef database update -c PersistedGrantDbContext
Both are required even if you don't use SAML (schema must match).
// ❌ Before (v7)
public class MyService
{
private readonly ICache<MyData> _cache;
public MyService(ICache<MyData> cache) => _cache = cache;
public async Task<MyData> GetAsync(string key)
{
return await _cache.GetOrAddAsync(key,
TimeSpan.FromMinutes(5),
() => LoadFromDbAsync(key));
}
}
// ✅ After (v8) — use Microsoft HybridCache
public class MyService
{
private readonly HybridCache _cache;
public MyService([FromKeyedServices("ConfigurationStoreCache")] HybridCache cache)
=> _cache = cache;
public async Task<MyData> GetAsync(string key, CancellationToken ct)
{
return await _cache.GetOrCreateAsync(key,
async token => await LoadFromDbAsync(key, token),
new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(5)
}, cancellationToken: ct);
}
}
Key: use keyed service "ConfigurationStoreCache" (ServiceProviderKeys.ConfigurationStoreCache). CachingOptions.CacheLockTimeout is obsolete.
// ❌ Before (v7)
public class MyService
{
private readonly IClock _clock;
public MyService(IClock clock) => _clock = clock;
public DateTime Now => _clock.UtcNow.UtcDateTime;
}
// ✅ After (v8)
public class MyService
{
private readonly TimeProvider _timeProvider;
public MyService(TimeProvider timeProvider) => _timeProvider = timeProvider;
public DateTime Now => _timeProvider.GetUtcNow().UtcDateTime;
}
Note: GetUtcNow() (method) replaces UtcNow (property).
All store and service interfaces now require CancellationToken ct as the last parameter:
// ❌ Before (v7)
public Task<Client?> FindClientByIdAsync(string clientId)
// ✅ After (v8)
public Task<Client?> FindClientByIdAsync(string clientId, CancellationToken ct)
Affected interfaces include: IClientStore, IResourceStore, IPersistedGrantStore, IDeviceFlowStore, ICorsPolicyService, IProfileService, and all custom stores/services.
Also: ICancellationTokenProvider is removed entirely.
// ✅ New required method
public IAsyncEnumerable<Client> GetAllClientsAsync(CancellationToken ct)
Used by Financial-Grade Security features and conformance reports.
// ❌ Before (v7) — individual parameters
public Task<string> CreateRefreshTokenAsync(
ClaimsPrincipal subject, Token accessToken, Client client)
// ✅ After (v8) — request objects
public Task<string> CreateRefreshTokenAsync(RefreshTokenCreationRequest request, CancellationToken ct)
public Task<string> UpdateRefreshTokenAsync(RefreshTokenUpdateRequest request, CancellationToken ct)
// ❌ Removed in v8 — use PAR (Pushed Authorization Requests) instead
services.AddTransient<IAuthorizationParametersMessageStore, MyStore>();
// ✅ PAR is the replacement for passing large authorization parameters
Nine interfaces changed IEnumerable<T> → IReadOnlyCollection<T>:
// ❌ Before
public Task<IEnumerable<ApiScope>> FindApiScopesByNameAsync(IEnumerable<string> scopeNames)
// ✅ After
public Task<IReadOnlyCollection<ApiScope>> FindApiScopesByNameAsync(
IEnumerable<string> scopeNames, CancellationToken ct)
// ❌ Typo in v7
DPoPProofValidatonContext → DPoPProofValidationContext
DPoPProofValidatonResult → DPoPProofValidationResult
// ❌ Before (v7)
var license = IdentityServerLicense.Current;
var edition = summary.LicenseEdition;
// ✅ After (v8)
var info = LicenseInformation.Current; // from Duende.IdentityServer.Licensing
var skus = summary.EntitledSkus; // collection replaces single edition
// ❌ Before (v7)
public IdentityProviderStore(IServiceProvider sp, ConfigurationDbContext ctx)
// ✅ After (v8) — new required parameter
public IdentityProviderStore(
IServiceProvider sp, ConfigurationDbContext ctx, IIdentityProviderFactory factory)
// ❌ Before (v7)
if (result.Error == AuthorizationError.LoginRequired) { }
// ✅ After (v8)
if (result.Error == InteractionError.LoginRequired) { }
Values remain the same: AccessDenied, LoginRequired, InteractionRequired.
// ❌ Before (v7)
await _interaction.DenyAuthorizationAsync(context, AuthorizationError.AccessDenied);
// ✅ After (v8) — now accepts IAuthenticationContext (protocol-agnostic for OIDC/SAML)
await _interaction.DenyAuthenticationAsync(context, InteractionError.AccessDenied);
// ❌ Before (v7)
var client = context.Client;
// ✅ After (v8)
var client = context.Application;
// ❌ Before (v7)
await _validator.ValidateAccessTokenAsync(token);
// ✅ After (v8) — new expectedScope parameter
await _validator.ValidateAccessTokenAsync(token, expectedScope: null, ct);
PreviewFeatureOptions and IdentityServerOptions.Preview are removed. Options relocated:
// ❌ Before (v7)
options.Preview.EnableDiscoveryDocumentCache = true;
options.Preview.DiscoveryDocumentCacheDuration = TimeSpan.FromMinutes(10);
options.Preview.StrictClientAssertionAudienceValidation = true;
// ✅ After (v8)
options.Discovery.EnableDiscoveryDocumentCache = true;
options.Discovery.DiscoveryDocumentCacheDuration = TimeSpan.FromMinutes(10);
options.StrictClientAssertionAudienceValidation = true; // default changed to false!
PersistedGrantFilter.ClientIds/Types: Now non-nullable with empty collection defaults. Replace null checks with .Count > 0.AddSamlSessionAsync, GetSamlSessionListAsync, RemoveSamlSessionAsyncRememberConsent always false during device flow (RFC 8628 security).Duende:IdentityServer:LicenseKey or Duende:LicenseKey in configuration.DPoPExtensions → DPoPServiceCollectionExtensions: Class renamed in JwtBearer package.IOperationalStoreNotification registered, uses single ExecuteDeleteAsync call (automatic improvement, no action needed).net10.0ConfigurationDbContext and PersistedGrantDbContext)ICache<T> → keyed HybridCacheIClock → TimeProviderCancellationToken to all async store/service methodsICancellationTokenProvider referencesGetAllClientsAsync to custom IClientStore (returns IAsyncEnumerable<Client>)IRefreshTokenService implementations (request objects)IAuthorizationParametersMessageStore (use PAR)IEnumerable<T> → IReadOnlyCollection<T> return typesIdentityServerLicense → LicenseInformation)AuthorizationError → InteractionErrorDenyAuthorizationAsync → DenyAuthenticationAsyncProfileDataRequestContext.Client → .ApplicationITokenValidator.ValidateAccessTokenAsync calls (add expectedScope param)PreviewFeatureOptions settings[FromKeyedServices("ConfigurationStoreCache")] — plain HybridCache injection gets a different instance.CancellationToken.None everywhere — propagate from the method parameter for proper request cancellation.IAuthorizationParametersMessageStore for large auth requests, switch clients to use PAR (require_pushed_authorization_requests).identityserver-configuration — IdentityServer host configuration and optionsidentityserver-stores — Store implementation patterns (affected by CancellationToken changes)identityserver-saml — SAML 2.0 support (new in v8, requires EF migration)identityserver-usermanagement — User Management (new in v8)Provides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.
npx claudepluginhub duendesoftware/duende-skills --plugin duende-skills