← Files Duende SkillsARCHIVED FILE

skills/identityserver-upgrade-v7-to-v8/SKILL.md

15.1 KB · Oct 5, 2026 · 18:31 UTC

↓ Download file

---
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."
invocable: false
---

# Upgrading IdentityServer v7 to v8

## When to Use This Skill

- Upgrading a Duende IdentityServer project from v7.4 to v8.0
- Fixing build errors after updating NuGet packages to v8
- Migrating custom stores/services to new v8 interfaces
- Running EF Core database migrations for v8 (SAML tables)
- Replacing deprecated APIs (ICache, IClock, IAuthorizationParametersMessageStore)

## Core Principles

- v8.0 requires **.NET 10** — update TFM before anything else
- All breaking changes are compile-time errors (no silent behavior changes)
- Migration is mechanical — find/replace patterns work for most changes
- Run EF migrations even if you don't use SAML (schema must match)
- **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.

Docs: https://docs.duendesoftware.com/identityserver/upgrades/v7_4-to-v8_0/

## Step-by-Step Migration

### 1. Update Target Framework

```xml
<!-- ❌ Before -->
<TargetFramework>net8.0</TargetFramework>

<!-- ✅ After -->
<TargetFramework>net10.0</TargetFramework>
```

### 2. Update NuGet Packages

Check [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:

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

### 3. Run EF Database Migrations

Two migrations are required — one for the Configuration Store and one for the Operational Store:

```bash
# 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).

### 4. Replace ICache<T> with HybridCache

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

### 5. Replace IClock with TimeProvider

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

### 6. Add CancellationToken to All Async Interfaces

All store and service interfaces now require `CancellationToken ct` as the last parameter:

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

### 7. Add GetAllClientsAsync to IClientStore

```csharp
// ✅ New required method
public IAsyncEnumerable<Client> GetAllClientsAsync(CancellationToken ct)
```

Used by Financial-Grade Security features and conformance reports.

### 8. Update Refresh Token Service

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

### 9. Remove IAuthorizationParametersMessageStore

```csharp
// ❌ Removed in v8 — use PAR (Pushed Authorization Requests) instead
services.AddTransient<IAuthorizationParametersMessageStore, MyStore>();

// ✅ PAR is the replacement for passing large authorization parameters
```

### 10. Fix Return Type Changes

Nine interfaces changed `IEnumerable<T>` → `IReadOnlyCollection<T>`:

```csharp
// ❌ Before
public Task<IEnumerable<ApiScope>> FindApiScopesByNameAsync(IEnumerable<string> scopeNames)

// ✅ After
public Task<IReadOnlyCollection<ApiScope>> FindApiScopesByNameAsync(
    IEnumerable<string> scopeNames, CancellationToken ct)
```

### 11. Fix DPoP Type Names

```csharp
// ❌ Typo in v7
DPoPProofValidatonContext  → DPoPProofValidationContext
DPoPProofValidatonResult   → DPoPProofValidationResult
```

### 12. Update Licensing Code

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

#### New v8 License Key Format

- v8 introduced a **new license key file format**: the v8 key is a signed **JWT carrying a `kid` header**.
- A **v7/earlier key still works with v8 core** — no new purchase is needed to run v8 core on an existing key.
- A **v8 key does NOT work on v7/earlier OR on the BFF Security Framework runtime**. It fails signature validation with Microsoft.IdentityModel error:
  - `IDX10503: Signature validation failed. Token does not have a kid.`
  - That exact error is the tell-tale sign of a **v8 key loaded into a v7 or BFF runtime**.
- **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.

#### Runtime License Enforcement Changed (behavioral reversal)

v8 validates feature usage at runtime. When a **license IS present but lacks the entitlement**, behavior splits into two tiers:

| Tier | Behavior when unlicensed | Features |
| ---- | ------------------------ | -------- |
| A | **THROWS** during startup validation | Server-Side Sessions, Automatic Key Management, SAML (IdP and Service Provider) |
| B | **LOGS a warning** (rate-limited ~once/5 min) | DPoP, Resource Isolation, CIBA, Dynamic Identity Providers, Financial-grade/Conformance, User Management |

- If **NO license is configured** (local dev / non-prod), Tier-A features **downgrade to logging** instead of throwing.
- **Guidance**: use your **production license key in lower environments** so entitlement gaps (e.g. Server-Side Sessions) surface before production.
- **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.

#### Editions → Plans

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

### 13. Update EF Identity Provider Store

```csharp
// ❌ Before (v7)
public IdentityProviderStore(IServiceProvider sp, ConfigurationDbContext ctx)

// ✅ After (v8) — new required parameter
public IdentityProviderStore(
    IServiceProvider sp, ConfigurationDbContext ctx, IIdentityProviderFactory factory)
```

### 14. Rename AuthorizationError → InteractionError

```csharp
// ❌ Before (v7)
if (result.Error == AuthorizationError.LoginRequired) { }

// ✅ After (v8)
if (result.Error == InteractionError.LoginRequired) { }
```

Values remain the same: `AccessDenied`, `LoginRequired`, `InteractionRequired`.

### 15. Rename DenyAuthorizationAsync → DenyAuthenticationAsync

```csharp
// ❌ Before (v7)
await _interaction.DenyAuthorizationAsync(context, AuthorizationError.AccessDenied);

// ✅ After (v8) — now accepts IAuthenticationContext (protocol-agnostic for OIDC/SAML)
await _interaction.DenyAuthenticationAsync(context, InteractionError.AccessDenied);
```

### 16. Rename ProfileDataRequestContext.Client → .Application

```csharp
// ❌ Before (v7)
var client = context.Client;

// ✅ After (v8)
var client = context.Application;
```

### 17. Update ITokenValidator.ValidateAccessTokenAsync

```csharp
// ❌ Before (v7)
await _validator.ValidateAccessTokenAsync(token);

// ✅ After (v8) — new expectedScope parameter
await _validator.ValidateAccessTokenAsync(token, expectedScope: null, ct);
```

### 18. Relocate PreviewFeatureOptions

`PreviewFeatureOptions` and `IdentityServerOptions.Preview` are removed. Options relocated:

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

## Other Notable Changes

- **NRT enabled**: All assemblies use nullable reference types. Fix nullable warnings.
- **HTTP 303**: POST endpoint redirects now unconditionally use 303 (FAPI 2.0 compliance).
- **`PersistedGrantFilter.ClientIds`/`Types`**: Now non-nullable with empty collection defaults. Replace null checks with `.Count > 0`.
- **IUserSession**: Three new SAML session methods added (implement as no-op if not using SAML):
  - `AddSamlSessionAsync`, `GetSamlSessionListAsync`, `RemoveSamlSessionAsync`
- **Log levels**: Secret validation failures changed from Error to Debug — update alerting to watch for Warning-level entries at endpoint level instead.
- **Device flow consent**: "Remember My Decision" no longer offered — `RememberConsent` always `false` during device flow (RFC 8628 security).
- **License key from IConfiguration**: IdentityServer now reads license key automatically from `Duende:IdentityServer:LicenseKey` or `Duende:LicenseKey` in configuration.
- **`DPoPExtensions` → `DPoPServiceCollectionExtensions`**: Class renamed in JwtBearer package.
- **Token cleanup performance**: When no `IOperationalStoreNotification` registered, uses single `ExecuteDeleteAsync` call (automatic improvement, no action needed).
- **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.

## Migration Checklist

1. ☐ Update TFM to `net10.0`
2. ☐ Update all Duende.* packages to latest stable 8.x (check [NuGet](https://www.nuget.org/packages/Duende.IdentityServer))
3. ☐ Run EF migrations (both `ConfigurationDbContext` and `PersistedGrantDbContext`)
4. ☐ Replace `ICache<T>` → keyed `HybridCache`
5. ☐ Replace `IClock` → `TimeProvider`
6. ☐ Add `CancellationToken` to all async store/service methods
7. ☐ Remove `ICancellationTokenProvider` references
8. ☐ Add `GetAllClientsAsync` to custom `IClientStore` (returns `IAsyncEnumerable<Client>`)
9. ☐ Update `IRefreshTokenService` implementations (request objects)
10. ☐ Remove `IAuthorizationParametersMessageStore` (use PAR)
11. ☐ Fix `IEnumerable<T>` → `IReadOnlyCollection<T>` return types
12. ☐ Fix DPoP type name typos
13. ☐ Update licensing references (`IdentityServerLicense` → `LicenseInformation`)
14. ☐ Rename `AuthorizationError` → `InteractionError`
15. ☐ Rename `DenyAuthorizationAsync` → `DenyAuthenticationAsync`
16. ☐ Rename `ProfileDataRequestContext.Client` → `.Application`
17. ☐ Update `ITokenValidator.ValidateAccessTokenAsync` calls (add `expectedScope` param)
18. ☐ Relocate `PreviewFeatureOptions` settings
19. ☐ Fix nullable reference type warnings
20. ☐ Test build and run

## Common Pitfalls

1. **Forgetting EF migration**: Even without SAML, the schema must be updated or EF will throw at runtime.
2. **HybridCache keyed service**: Must use `[FromKeyedServices("ConfigurationStoreCache")]` — plain `HybridCache` injection gets a different instance.
3. **CancellationToken propagation**: Don't pass `CancellationToken.None` everywhere — propagate from the method parameter for proper request cancellation.
4. **GetAllClientsAsync performance**: Return all clients from your store; used rarely but must be implemented.
5. **PAR migration**: If you used `IAuthorizationParametersMessageStore` for large auth requests, switch clients to use PAR (`require_pushed_authorization_requests`).
6. **`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.
7. **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.

## Related Skills

- `identityserver-configuration` — IdentityServer host configuration and options
- `identityserver-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)

SHA-256: 855c489fd5eb916cfc51da80d30295d68e131a9eadb47637e869289e8636e531