← Duende SkillsCONTENT HISTORY

Update to Duende Skills

Snapshot Sep 30, 2026 · 23:14 UTC · version 0.3.0

Collection source: not recorded for this historical snapshot.

WHAT CHANGED · RULE-BASED ANALYSIS

First saved snapshot

No earlier snapshot is available to establish a change.

Compare saved observations

Download comparison JSON
Full technical diff · 0 changed fields
Full snapshot data
{
  "name": "identityserver-upgrade-v7-to-v8",
  "description": "Migrating Duende IdentityServer from v7.4 to v8.0: breaking changes, API replacements (ICache→HybridCache, IClock→TimeProvider), CancellationToken additions, EF migrations, and step-by-step upgrade guide.",
  "included_files": [],
  "skill_md_contents": "---\nname: identityserver-upgrade-v7-to-v8\ndescription: \"Migrating Duende IdentityServer from v7.4 to v8.0: breaking changes, API replacements (ICache→HybridCache, IClock→TimeProvider), CancellationToken additions, EF migrations, and step-by-step upgrade guide.\"\ninvocable: false\n---\n\n# Upgrading IdentityServer v7 to v8\n\n## When to Use This Skill\n\n- Upgrading a Duende IdentityServer project from v7.4 to v8.0\n- Fixing build errors after updating NuGet packages to v8\n- Migrating custom stores/services to new v8 interfaces\n- Running EF Core database migrations for v8 (SAML tables)\n- Replacing deprecated APIs (ICache, IClock, IAuthorizationParametersMessageStore)\n\n## Core Principles\n\n- v8.0 requires **.NET 10** — update TFM before anything else\n- All breaking changes are compile-time errors (no silent behavior changes)\n- Migration is mechanical — find/replace patterns work for most changes\n- Run EF migrations even if you don't use SAML (schema must match)\n- **Always check the latest stable 8.x package version** on [NuGet](https://www.nuget.org/packages/Duende.IdentityServer) before upgrading — do not hardcode `8.0.1`; use whatever the latest stable (non-prerelease) 8.x version is at the time of the upgrade.\n\nDocs: https://docs.duendesoftware.com/identityserver/upgrades/v7_4-to-v8_0/\n\n## Step-by-Step Migration\n\n### 1. Update Target Framework\n\n```xml\n<!-- ❌ Before -->\n<TargetFramework>net8.0</TargetFramework>\n\n<!-- ✅ After -->\n<TargetFramework>net10.0</TargetFramework>\n```\n\n### 2. Update NuGet Packages\n\nCheck [NuGet](https://www.nuget.org/packages/Duende.IdentityServer) for the latest stable 8.x version. At time of writing, that is `8.0.1`, but use whatever is current:\n\n```xml\n<PackageReference Include=\"Duende.IdentityServer\" Version=\"8.0.1\" />\n<PackageReference Include=\"Duende.IdentityServer.EntityFramework\" Version=\"8.0.1\" />\n<!-- Update all Duende.* packages to the latest stable 8.x version -->\n```\n\n### 3. Run EF Database Migrations\n\nTwo migrations are required — one for the Configuration Store and one for the Operational Store:\n\n```bash\n# Configuration Store — adds 7 SAML-related tables\ndotnet ef migrations add Update_DuendeIdentityServer_v8_0 \\\n    -c ConfigurationDbContext -o Migrations/ConfigurationDb\ndotnet ef database update -c ConfigurationDbContext\n\n# Operational Store — adds 3 SAML session tables\ndotnet ef migrations add Update_DuendeIdentityServer_v8_0_Saml \\\n    -c PersistedGrantDbContext -o Migrations/PersistedGrantDb\ndotnet ef database update -c PersistedGrantDbContext\n```\n\nBoth are required even if you don't use SAML (schema must match).\n\n### 4. Replace ICache<T> with HybridCache\n\n```csharp\n// ❌ Before (v7)\npublic class MyService\n{\n    private readonly ICache<MyData> _cache;\n    public MyService(ICache<MyData> cache) => _cache = cache;\n\n    public async Task<MyData> GetAsync(string key)\n    {\n        return await _cache.GetOrAddAsync(key,\n            TimeSpan.FromMinutes(5),\n            () => LoadFromDbAsync(key));\n    }\n}\n\n// ✅ After (v8) — use Microsoft HybridCache\npublic class MyService\n{\n    private readonly HybridCache _cache;\n    public MyService([FromKeyedServices(\"ConfigurationStoreCache\")] HybridCache cache)\n        => _cache = cache;\n\n    public async Task<MyData> GetAsync(string key, CancellationToken ct)\n    {\n        return await _cache.GetOrCreateAsync(key,\n            async token => await LoadFromDbAsync(key, token),\n            new HybridCacheEntryOptions\n            {\n                Expiration = TimeSpan.FromMinutes(5)\n            }, cancellationToken: ct);\n    }\n}\n```\n\nKey: use keyed service `\"ConfigurationStoreCache\"` (`ServiceProviderKeys.ConfigurationStoreCache`). `CachingOptions.CacheLockTimeout` is obsolete.\n\n### 5. Replace IClock with TimeProvider\n\n```csharp\n// ❌ Before (v7)\npublic class MyService\n{\n    private readonly IClock _clock;\n    public MyService(IClock clock) => _clock = clock;\n    public DateTime Now => _clock.UtcNow.UtcDateTime;\n}\n\n// ✅ After (v8)\npublic class MyService\n{\n    private readonly TimeProvider _timeProvider;\n    public MyService(TimeProvider timeProvider) => _timeProvider = timeProvider;\n    public DateTime Now => _timeProvider.GetUtcNow().UtcDateTime;\n}\n```\n\nNote: `GetUtcNow()` (method) replaces `UtcNow` (property).\n\n### 6. Add CancellationToken to All Async Interfaces\n\nAll store and service interfaces now require `CancellationToken ct` as the last parameter:\n\n```csharp\n// ❌ Before (v7)\npublic Task<Client?> FindClientByIdAsync(string clientId)\n\n// ✅ After (v8)\npublic Task<Client?> FindClientByIdAsync(string clientId, CancellationToken ct)\n```\n\nAffected interfaces include: `IClientStore`, `IResourceStore`, `IPersistedGrantStore`, `IDeviceFlowStore`, `ICorsPolicyService`, `IProfileService`, and all custom stores/services.\n\nAlso: `ICancellationTokenProvider` is removed entirely.\n\n### 7. Add GetAllClientsAsync to IClientStore\n\n```csharp\n// ✅ New required method\npublic IAsyncEnumerable<Client> GetAllClientsAsync(CancellationToken ct)\n```\n\nUsed by Financial-Grade Security features and conformance reports.\n\n### 8. Update Refresh Token Service\n\n```csharp\n// ❌ Before (v7) — individual parameters\npublic Task<string> CreateRefreshTokenAsync(\n    ClaimsPrincipal subject, Token accessToken, Client client)\n\n// ✅ After (v8) — request objects\npublic Task<string> CreateRefreshTokenAsync(RefreshTokenCreationRequest request, CancellationToken ct)\npublic Task<string> UpdateRefreshTokenAsync(RefreshTokenUpdateRequest request, CancellationToken ct)\n```\n\n### 9. Remove IAuthorizationParametersMessageStore\n\n```csharp\n// ❌ Removed in v8 — use PAR (Pushed Authorization Requests) instead\nservices.AddTransient<IAuthorizationParametersMessageStore, MyStore>();\n\n// ✅ PAR is the replacement for passing large authorization parameters\n```\n\n### 10. Fix Return Type Changes\n\nNine interfaces changed `IEnumerable<T>` → `IReadOnlyCollection<T>`:\n\n```csharp\n// ❌ Before\npublic Task<IEnumerable<ApiScope>> FindApiScopesByNameAsync(IEnumerable<string> scopeNames)\n\n// ✅ After\npublic Task<IReadOnlyCollection<ApiScope>> FindApiScopesByNameAsync(\n    IEnumerable<string> scopeNames, CancellationToken ct)\n```\n\n### 11. Fix DPoP Type Names\n\n```csharp\n// ❌ Typo in v7\nDPoPProofValidatonContext  → DPoPProofValidationContext\nDPoPProofValidatonResult   → DPoPProofValidationResult\n```\n\n### 12. Update Licensing Code\n\n```csharp\n// ❌ Before (v7)\nvar license = IdentityServerLicense.Current;\nvar edition = summary.LicenseEdition;\n\n// ✅ After (v8)\nvar info = LicenseInformation.Current;  // from Duende.IdentityServer.Licensing\nvar skus = summary.EntitledSkus;        // collection replaces single edition\n```\n\n#### New v8 License Key Format\n\n- v8 introduced a **new license key file format**: the v8 key is a signed **JWT carrying a `kid` header**.\n- A **v7/earlier key still works with v8 core** — no new purchase is needed to run v8 core on an existing key.\n- A **v8 key does NOT work on v7/earlier OR on the BFF Security Framework runtime**. It fails signature validation with Microsoft.IdentityModel error:\n  - `IDX10503: Signature validation failed. Token does not have a kid.`\n  - That exact error is the tell-tale sign of a **v8 key loaded into a v7 or BFF runtime**.\n- **Add-ons require a v8-format key in production**: using **SAML** or **Duende User Management** in production on v8 REQUIRES a new v8-format license key. Older-format keys run v8 core, but not these add-ons in production.\n\n#### Runtime License Enforcement Changed (behavioral reversal)\n\nv8 validates feature usage at runtime. When a **license IS present but lacks the entitlement**, behavior splits into two tiers:\n\n| Tier | Behavior when unlicensed | Features |\n| ---- | ------------------------ | -------- |\n| A | **THROWS** during startup validation | Server-Side Sessions, Automatic Key Management, SAML (IdP and Service Provider) |\n| B | **LOGS a warning** (rate-limited ~once/5 min) | DPoP, Resource Isolation, CIBA, Dynamic Identity Providers, Financial-grade/Conformance, User Management |\n\n- If **NO license is configured** (local dev / non-prod), Tier-A features **downgrade to logging** instead of throwing.\n- **Guidance**: use your **production license key in lower environments** so entitlement gaps (e.g. Server-Side Sessions) surface before production.\n- **Contrast with v7 and earlier**: those versions **disabled** some features at runtime when unlicensed (Server-Side Sessions, DPoP, Resource Isolation, PAR, Dynamic Identity Providers, CIBA). **v8 no longer disables** — it logs or throws per the tiers above.\n\n#### Editions → Plans\n\nThe product moved from fixed **Starter / Business / Enterprise** editions to generic **plans**. The old three editions are still honored for legacy/long-term customers only. The **Community edition remains**. Update any code or docs that hard-code \"three editions\" to reflect the plan model.\n\n### 13. Update EF Identity Provider Store\n\n```csharp\n// ❌ Before (v7)\npublic IdentityProviderStore(IServiceProvider sp, ConfigurationDbContext ctx)\n\n// ✅ After (v8) — new required parameter\npublic IdentityProviderStore(\n    IServiceProvider sp, ConfigurationDbContext ctx, IIdentityProviderFactory factory)\n```\n\n### 14. Rename AuthorizationError → InteractionError\n\n```csharp\n// ❌ Before (v7)\nif (result.Error == AuthorizationError.LoginRequired) { }\n\n// ✅ After (v8)\nif (result.Error == InteractionError.LoginRequired) { }\n```\n\nValues remain the same: `AccessDenied`, `LoginRequired`, `InteractionRequired`.\n\n### 15. Rename DenyAuthorizationAsync → DenyAuthenticationAsync\n\n```csharp\n// ❌ Before (v7)\nawait _interaction.DenyAuthorizationAsync(context, AuthorizationError.AccessDenied);\n\n// ✅ After (v8) — now accepts IAuthenticationContext (protocol-agnostic for OIDC/SAML)\nawait _interaction.DenyAuthenticationAsync(context, InteractionError.AccessDenied);\n```\n\n### 16. Rename ProfileDataRequestContext.Client → .Application\n\n```csharp\n// ❌ Before (v7)\nvar client = context.Client;\n\n// ✅ After (v8)\nvar client = context.Application;\n```\n\n### 17. Update ITokenValidator.ValidateAccessTokenAsync\n\n```csharp\n// ❌ Before (v7)\nawait _validator.ValidateAccessTokenAsync(token);\n\n// ✅ After (v8) — new expectedScope parameter\nawait _validator.ValidateAccessTokenAsync(token, expectedScope: null, ct);\n```\n\n### 18. Relocate PreviewFeatureOptions\n\n`PreviewFeatureOptions` and `IdentityServerOptions.Preview` are removed. Options relocated:\n\n```csharp\n// ❌ Before (v7)\noptions.Preview.EnableDiscoveryDocumentCache = true;\noptions.Preview.DiscoveryDocumentCacheDuration = TimeSpan.FromMinutes(10);\noptions.Preview.StrictClientAssertionAudienceValidation = true;\n\n// ✅ After (v8)\noptions.Discovery.EnableDiscoveryDocumentCache = true;\noptions.Discovery.DiscoveryDocumentCacheDuration = TimeSpan.FromMinutes(10);\noptions.StrictClientAssertionAudienceValidation = true;  // default changed to false!\n```\n\n## Other Notable Changes\n\n- **NRT enabled**: All assemblies use nullable reference types. Fix nullable warnings.\n- **HTTP 303**: POST endpoint redirects now unconditionally use 303 (FAPI 2.0 compliance).\n- **`PersistedGrantFilter.ClientIds`/`Types`**: Now non-nullable with empty collection defaults. Replace null checks with `.Count > 0`.\n- **IUserSession**: Three new SAML session methods added (implement as no-op if not using SAML):\n  - `AddSamlSessionAsync`, `GetSamlSessionListAsync`, `RemoveSamlSessionAsync`\n- **Log levels**: Secret validation failures changed from Error to Debug — update alerting to watch for Warning-level entries at endpoint level instead.\n- **Device flow consent**: \"Remember My Decision\" no longer offered — `RememberConsent` always `false` during device flow (RFC 8628 security).\n- **License key from IConfiguration**: IdentityServer now reads license key automatically from `Duende:IdentityServer:LicenseKey` or `Duende:LicenseKey` in configuration.\n- **`DPoPExtensions` → `DPoPServiceCollectionExtensions`**: Class renamed in JwtBearer package.\n- **Token cleanup performance**: When no `IOperationalStoreNotification` registered, uses single `ExecuteDeleteAsync` call (automatic improvement, no action needed).\n- **Orphaned grants revoked on session overwrite**: When server-side sessions enabled and session cookie reused by different user, previous user's grants are automatically revoked.\n\n## Migration Checklist\n\n1. ☐ Update TFM to `net10.0`\n2. ☐ Update all Duende.* packages to latest stable 8.x (check [NuGet](https://www.nuget.org/packages/Duende.IdentityServer))\n3. ☐ Run EF migrations (both `ConfigurationDbContext` and `PersistedGrantDbContext`)\n4. ☐ Replace `ICache<T>` → keyed `HybridCache`\n5. ☐ Replace `IClock` → `TimeProvider`\n6. ☐ Add `CancellationToken` to all async store/service methods\n7. ☐ Remove `ICancellationTokenProvider` references\n8. ☐ Add `GetAllClientsAsync` to custom `IClientStore` (returns `IAsyncEnumerable<Client>`)\n9. ☐ Update `IRefreshTokenService` implementations (request objects)\n10. ☐ Remove `IAuthorizationParametersMessageStore` (use PAR)\n11. ☐ Fix `IEnumerable<T>` → `IReadOnlyCollection<T>` return types\n12. ☐ Fix DPoP type name typos\n13. ☐ Update licensing references (`IdentityServerLicense` → `LicenseInformation`)\n14. ☐ Rename `AuthorizationError` → `InteractionError`\n15. ☐ Rename `DenyAuthorizationAsync` → `DenyAuthenticationAsync`\n16. ☐ Rename `ProfileDataRequestContext.Client` → `.Application`\n17. ☐ Update `ITokenValidator.ValidateAccessTokenAsync` calls (add `expectedScope` param)\n18. ☐ Relocate `PreviewFeatureOptions` settings\n19. ☐ Fix nullable reference type warnings\n20. ☐ Test build and run\n\n## Common Pitfalls\n\n1. **Forgetting EF migration**: Even without SAML, the schema must be updated or EF will throw at runtime.\n2. **HybridCache keyed service**: Must use `[FromKeyedServices(\"ConfigurationStoreCache\")]` — plain `HybridCache` injection gets a different instance.\n3. **CancellationToken propagation**: Don't pass `CancellationToken.None` everywhere — propagate from the method parameter for proper request cancellation.\n4. **GetAllClientsAsync performance**: Return all clients from your store; used rarely but must be implemented.\n5. **PAR migration**: If you used `IAuthorizationParametersMessageStore` for large auth requests, switch clients to use PAR (`require_pushed_authorization_requests`).\n6. **`IDX10503` after dropping in a v8 key**: A v8-format license key (signed JWT with a `kid` header) fails signature validation on v7/earlier or the **BFF Security Framework runtime** with `IDX10503: Signature validation failed. Token does not have a kid.` Keep the v7-format key for those runtimes — it still works on v8 core; only SAML/User Management add-ons in production require the new v8-format key.\n7. **Entitlement gaps surface late**: v8 no longer silently disables unlicensed features — Server-Side Sessions, Automatic Key Management, and SAML now **throw at startup** when a license is present but missing the entitlement. Run lower environments with the production license key to catch this before deploying.\n\n## Related Skills\n\n- `identityserver-configuration` — IdentityServer host configuration and options\n- `identityserver-stores` — Store implementation patterns (affected by CancellationToken changes)\n- `identityserver-saml` — SAML 2.0 support (new in v8, requires EF migration)\n- `identityserver-usermanagement` — User Management (new in v8)\n"
}

SHA-256: f09e95857ea3e3e8f9456956f428018fde076b58db0f4cc8e4f517a81ff0dffc