IdentityServer4 → Duende: What Actually Breaks (We Migrated the Quickstart and Logged It)
We migrated the public IdentityServer4 EF quickstart to Duende IdentityServer 8 on .NET 10 and logged every failure. Licensing, keys, grants, rollback.
Most IdentityServer4 → Duende write-ups tell you what should change. We wanted a record of what does break, so we took the public IdentityServer4 EntityFramework quickstart and migrated it to Duende IdentityServer 8.0.9 on .NET 10, logging every failure the moment it happened.
Everything below can be checked in the repository: github.com/Code-Majesty-Tech/identityserver4-to-duende-migration. It contains the untouched before/ app, the migrated after/ app, the timestamped migration log, a cutover plan and a test harness.
Three things to know before you read the numbers:
- This is not a client engagement. It’s the public quickstart from the archived IdentityServer4 repository (
archivebranch, commit5e40bd18), migrated by us. No customer data, no production traffic. - The work was done by an AI coding agent (Claude Code) operated by us, following a written brief. The durations quoted are wall-clock times of that session, taken from the log’s UTC timestamps. They aren’t estimates of how long a person would take.
- The quickstart is small. Two clients, one API, test users, the EF configuration and operational stores. Your system has more surface; the breakages below are a floor, not a ceiling.
The short version
| What we checked | Result | Session time (from log) |
|---|---|---|
| Licensing with no key | Warnings only, nothing blocked. As migrated, the app uses an edition-gated feature by default. | 2 min 37 s |
| .NET 10 first, IdentityServer4 unchanged | Blocked. IS4’s EF stores crash at startup on .NET 8 and .NET 10. | 8 min 17 s |
| Package + namespace swap | 104 compile errors, then 31 real API changes, then an EF startup failure | 11 min 50 s active |
| Signing keys / token compatibility | No compatibility in either direction until we gave Duende the IS4 key | 10 min 36 s |
| Live grants across the schema migration | All survived and work on Duende. Rollback is one-way for new grants. | 3 min 28 s |
| Unchanged MVC client | First login succeeded with no client change | 0 min 35 s |
| Front/back-channel logout | Same behaviour; one token header changed | 2 min 50 s |
Getting the old quickstart running at all took 14 min 58 s: it’s actually on IdentityServer4 4.0.0 and netcoreapp3.1, and it hard-codes SQL Server LocalDB, which is Windows-only. Step 2 also spans a 69-minute pause in the session; the 11 min 50 s is the active time with that gap removed (the log shows both).
1. Licensing: what Duende says when you run without a key
We ran the migrated app with no license key. Nothing was blocked: every request succeeded. Duende logged one Error at startup and one Warning the first time each licensed feature or counter was hit:
[Error] You do not have a valid license key for the Duende software. This is allowed for development
and testing scenarios. If you are running in production you are required to have a licensed version. …
[Warning] Automatic Key Management is being used but no Duende license is configured. …
[Warning] Issuer Count detected (1 registered) but no Duende license is configured. …
[Warning] PAR (RFC 9126) is being used but no Duende license is configured. …
[Warning] Client ID Count detected (1 registered) but no Duende license is configured. …
On graceful shutdown, Duende also writes a diagnostics summary with a LicenseUsageSummary. Ours read "ClientsUsedCount":2, "FeaturesUsed":["PAR (RFC 9126)","Automatic Key Management"].
What Duende’s licensing documentation says (fetched October 5, 2026):
- IdentityServer “is free for development, testing and personal projects, but production use requires a license.” There’s no client-count or request threshold under which production is free; the free route for production is the separately approved Community Edition, which has revenue and capital eligibility criteria.
- In v8, unlicensed use has “no request limit and no automatic shutdown.”
- With a license present, using Server Side Sessions, Automatic Key Management or SAML without the entitlement makes IdentityServer throw during startup validation. We couldn’t test that ourselves without a key, so treat it as cited, not demonstrated.
Two things in our run matter for which edition you buy:
- Automatic Key Management was in use by default. The quickstart never asked for it, but Duende enables it unless you turn it off. The pricing page (fetched October 5, 2026) lists it as included in Advanced and as an add-on for Standard; it isn’t listed for Lite. When we switched to signing with the old IS4 key instead (section 4), the warning disappeared and
FeaturesUseddropped to PAR only. - PAR showed up because the client turned it on. The quickstart never configured Pushed Authorization Requests. After we retargeted the MVC client to .NET 10, the ASP.NET Core OIDC handler started using PAR automatically, because Duende advertises a PAR endpoint. The pricing page puts PAR under “Core Protocol Support”, which is listed for Lite, the entry edition.
One discrepancy we recorded but didn’t chase: the docs describe the startup error as appearing when a key is “present but invalid.” We had no key at all and still got the Error-level line.
2. The runtime upgrade can’t go first
The conventional advice is to change one thing at a time: move the runtime first, then swap IdentityServer. We tried exactly that, retargeting to .NET 10 with IdentityServer4 4.1.2 untouched.
It compiled. It didn’t start:
System.TypeInitializationException: The type initializer for
'IdentityServer4.EntityFramework.Mappers.ClientMappers' threw an exception.
---> System.ArgumentException: GenericArguments[0], 'System.Char', on
'T MaxFloat[T](System.Collections.Generic.IEnumerable`1[T])' violates the constraint of type 'T'.
The stack trace runs through AutoMapper 10’s TypeDetails.BuildPublicNoArgExtensionMethods into MakeGenericMethod. IdentityServer4.EntityFramework.Storage 4.1.2 pins AutoMapper to [10.0.0, 11.0.0):
- AutoMapper 10.1.1, the newest version allowed, fails the same way.
- AutoMapper 12.0.1 fixes that error but breaks IS4’s compiled mapping profiles (
MissingMethodException: … IMappingExpressionBase3.ConstructUsing`). - A control run on .NET 8 failed identically. Of the runtimes we tried (6, 8 and 10), only .NET 6 worked.
Duende.IdentityServer.EntityFramework.Storage 8.0.9 doesn’t depend on AutoMapper at all. If you use IS4’s EF stores, the .NET 8+ upgrade and the Duende migration ship together.
The .NET 10 SDK’s NuGet audit also flagged two advisories against IdentityServer4 4.1.2, both “IdentityServer Open Redirect vulnerability” (GHSA-ff4q-64jc-gx98, GHSA-55p7-v223-x366), with no patched IdentityServer4 version. The .NET 6 SDK we built the baseline with has no NuGet audit, so it never showed them.
3. Package and namespace swap: what the compiler said
Following Duende’s upgrade guide:
- Package swap only: 104 errors, all CS0246, across 19 files.
- After rewriting
using IdentityServer4…: 5 left. Four wereIdentityModel: IS4 brought the package in transitively, and Duende 8 doesn’t.Duende.IdentityModelreplaces it. The fifth was a Razor view the regex missed because the file starts with a UTF-8 BOM. - Then the real API changes, 31 errors:
- A required
CancellationToken ctparameter on interaction and event services. We had to pass a token at 29 call sites: 17 surfaced first, and 12 more appeared once those compiled. - Fully qualified
IdentityServer4.IdentityServerConstantsreferences. AuthorizationErroris nowInteractionError.HttpContext.GetSchemeSupportsSignOutAsyncandBase64Urlare gone.
- A required
- First run: EF Core 10 refused to start (
PendingModelChangesWarning) because the 2020 IS4 migrations no longer match Duende’s model. That happens even on an empty database. The guide’sdotnet ef migrations add UpdateToDuende_v8_0for both contexts fixes it.
Read the scaffolded configuration migration before running it. EF prints a data-loss warning for RedirectUri and PostLogoutRedirectUri shrinking from 2000 to 400 characters, which the guide mentions. It also replaces 15 indexes with unique composite indexes, such as ClientRedirectUris(ClientId, RedirectUri) and ClientScopes(ClientId, Scope). A database with a duplicate redirect URI or scope row on one client will fail there. The quickstart’s data doesn’t, so this is a pre-flight check, not something we hit.
Once running, two protocol-level changes were visible: authorize and end-session now answer 303 instead of 302 (listed in the guide’s v8 breaking changes), and the .NET 10 client switched to PAR.
4. Signing keys: the outage most write-ups skip
The quickstart signs with AddDeveloperSigningCredential(), which writes a key to tempkey.jwk at runtime. That file isn’t in source control and doesn’t ship with a new build, so the migrated app quietly generated a different key. Duende also enables automatic key management and publishes a managed key alongside it.
We issued a token on the IS4 side and validated it against the Duende side:
| Scenario | Result |
|---|---|
| IS4-issued token → API on the migrated stack | 401 The signature key was not found |
| Duende-issued token → API after rolling back to IS4 | 401, retries don’t help |
| API left running across the cutover | Old tokens pass from cache. The first token with the new kid gets a 401 and triggers a metadata refresh; after that, new tokens pass and every old token fails. |
So as migrated, every outstanding access token dies the moment an API refreshes its keys, and a rollback kills everything the new server issued.
Duende 8 loaded IS4’s tempkey.jwk without complaint, so the fix is to hand it the old key. We made after/ configurable:
| Signing mode | IS4 token → Duende side | Duende token → IS4 side (rollback) |
|---|---|---|
legacy: Duende signs with the IS4 key, automatic key management off | 200 | 200 |
automatic: managed key signs, IS4 key published validation-only | 200 | 401 |
legacy is the only configuration we tested where rollback is invisible to APIs. It also avoids the automatic key management licensing question from section 1.
5. Live grants: they survive, but rollback is one-way
To test grant survival we needed real grants. The stock quickstart never writes a long-lived one: no client has offline access, there’s no consent, and there are no reference tokens. So we inserted two test clients into the IS4 configuration database with SQL (no code change). Then we issued four refresh tokens and a reference token from the running IS4 app, cloned the database, and let Duende migrate the clone.
- The operational-store migration is additive: it drops the primary key on
Key, adds anId bigint IDENTITYkey and new tables. All five grants survived with byte-identical data. - An IS4 refresh token redeemed on Duende (200). Reusing it was rejected (
invalid_grant), as OneTimeOnly requires. The IS4 reference token worked at Duende’s userinfo endpoint. - Duende writes rotated tokens in its own format: a different key format and a Data-Protected payload (
{"PersistentGrantDataContainerVersion":1,"DataProtected":true,…}). It kept the original creation time, so the 30-day absolute lifetime didn’t reset. - Rollback: IS4 4.1.2 started fine on the migrated schema, logged users in, redeemed untouched IS4 tokens and wrote new grants. It could not use a refresh token Duende had rotated:
found in database: False, theninvalid_grant. That token worked again once we rolled forward.
The rollback cost is concrete: every user whose refresh token was used on Duende has to log in again. Duende-written grants also depend on the Data Protection key ring, which is why the upgrade guide tells you to set the application name explicitly. We cite that rather than demonstrate it.
6. The unchanged client, and logout
We ran the unchanged .NET 6 MVC client from before/ against the Duende server and API. The first interactive login succeeded with no client change. It sent PKCE (S256), didn’t use PAR, and saw no consent screen. Nothing broke.
With front- and back-channel logout URIs configured, logout behaved the same on both sides: the MVC session ended for both RP-initiated and IdP-initiated logout, and back-channel notifications arrived. One wire-level difference: IS4 4.1.2 sent back-channel logout tokens with header typ: at+jwt; Duende sends typ: logout+jwt and adds a logout_reason claim. Check any hand-written back-channel receiver that pins typ.
What we would actually do
The full plan is docs/CUTOVER.md in the repo. In short:
- Cut over with Duende signing with the IS4 key. Tokens stay interchangeable both ways, and rollback means running IS4 against the same migrated database.
- Close the rollback window deliberately. Its cost is re-login for users whose refresh tokens Duende touched.
- Only then move to automatic key management, keeping the IS4 key validation-only for at least your longest access-token lifetime. There’s no IS4 rollback after this step.
The repository includes a test harness (after/tests/MigrationHarness.Tests) that encodes the token-compatibility, grant-survival and unchanged-client scenarios. In our final run, 10 tests passed. The three expected failures (naive key setup, rollback with automatic keys, Duende-rotated refresh tokens on IS4) failed for the reasons above.
If you’re working through this on your own system, our IdentityServer4 → Duende migration checklist covers the full inventory. If you’d rather have us run it on your codebase, that’s the IdentityServer migration sprint.
Related Posts
Duende IdentityServer 8 Example: Server, API and MVC Client on .NET 10 (Real Code)
A working Duende IdentityServer 8.0.9 example on .NET 10 — the server with EF stores, a client-credentials client, an MVC code-flow client and a JWT-protected API — quoted from a public repo you can run.
Why You Need to Migrate from .NET to .NET Core for Software Development
Discover the benefits of migrating from .NET to .NET Core for enhanced performance, cross-platform capabilities, improved security, and modern cloud-native development.
Azure DevOps Deployment Best Practices for .NET and .NET Core Applications
Learn essential Azure DevOps deployment best practices for .NET and .NET Core applications, including build pipelines, release pipelines, automated testing, and configuration management.