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.
Rewritten October 2026. The 2024 version of this post was an introduction to the concepts. This version keeps the concepts and adds what people actually search for: a complete, running Duende IdentityServer example, with every code block taken from a public repository.
Duende IdentityServer is the framework for running your own OpenID Connect and OAuth 2.0 token service in .NET. It is the commercial successor to IdentityServer4, built by the same authors, and it is what you migrate to now that IdentityServer4 is end-of-life.
Every snippet below comes from github.com/Code-Majesty-Tech/identityserver4-to-duende-migration, pinned to commit ddbc43e. The after/ folder is the public IdentityServer4 EntityFramework quickstart migrated to Duende.IdentityServer 8.0.9 on .NET 10. It contains four projects:
| Project | What it is |
|---|---|
IdentityServer | The token service, with EF Core configuration and operational stores on SQL Server |
Client | A console app using the client credentials flow (machine to machine) |
MvcClient | An ASP.NET Core MVC app using the authorization code flow (interactive login) |
Api | A web API that accepts the access tokens and checks the api1 scope |
If you want the whole thing running first, the README has the exact commands; SQL Server runs from the repo’s docker-compose.yml and scripts/run.sh after starts the migrated stack.
When should I choose Duende IdentityServer?
If you have more than one application that needs to log users in or call APIs on their behalf — web apps, SPAs, native apps, backend services, APIs calling other APIs — and you want one place that issues the tokens all of them trust, you want a token service. Duende IdentityServer is that, as a library you host rather than a service you rent. The alternative is a hosted identity provider; the trade-off is control and data ownership against operational responsibility.
Evaluating IdentityServer for your stack — or still running IdentityServer4 after its end-of-life? Start with our IdentityServer4 → Duende migration checklist to scope the move; the hands-on side is exactly what our Duende IdentityServer consulting sessions cover: migration scoping, integration architecture, and licensing guidance from engineers who run IdentityServer in production.
Let’s define the terms first
In a nutshell, Duende IdentityServer issues security tokens. The pieces of that system, as plainly as possible:
User is a human using a registered client to access resources.
Resources are the things you protect: identity data about users, and APIs.
Identity data (claims) are facts about a user — name, email, phone number — carried in tokens as claims.
APIs are functionality a client or user wants to call, usually a web API.
Client is a piece of software that requests tokens from IdentityServer, either to authenticate a user (an identity token) or to access a resource (an access token). A client must be registered before it can ask for anything. Clients come in many shapes — web apps, native apps, SPAs, server processes — but fall into two categories:
- Machine-to-machine. Two systems talking directly with no user involved: background jobs, daemons, service-to-service calls. IdentityServer issues a token to the calling machine. In protocol terms this is the client credentials flow. In the example, that is the
Clientconsole app. - Interactive applications. Web apps, SPAs and native apps where a person logs in through a browser. In protocol terms, the authorization code flow. In the example, that is
MvcClient.
Identity token is the result of an authentication event: who the user is (the sub claim), how and when they authenticated, plus optional identity claims.
Access token is permission to call an API. The client obtains it and sends it to the API; the API reads the client and user information inside it to decide what to allow.
Scope is a named slice of what a client is allowed to ask for — openid and profile for identity data, or an API scope like api1 for an API. Clients are registered with the scopes they may request; APIs check that the token carries the scope they require.
The example, file by file
1. The server: AddIdentityServer, EF stores, and the signing-key decision
From after/src/IdentityServer/Startup.cs:
var signingMode = Configuration["SigningKeys:Mode"] ?? "automatic";
var legacyKeyPath = Configuration["SigningKeys:LegacyIs4KeyPath"];
var builder = services.AddIdentityServer(options =>
{
options.KeyManagement.Enabled = signingMode != "legacy";
})
.AddTestUsers(TestUsers.Users)
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = b => b.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = b => b.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
});
if (!string.IsNullOrEmpty(legacyKeyPath))
{
var legacyKey = LegacySigningKey.Load(legacyKeyPath);
if (signingMode == "legacy")
builder.AddSigningCredential(legacyKey, SecurityAlgorithms.RsaSha256);
else
builder.AddValidationKey(legacyKey, IdentityServerConstants.RsaSigningAlgorithm.RS256);
}
Three things to notice:
- Two stores. The configuration store holds clients, scopes and resources (the things in
Config.csbelow, once seeded). The operational store holds runtime data: persisted grants such as refresh tokens, and — new in Duende — managed signing keys and server-side sessions. Both are EF CoreDbContexts with their own migrations. KeyManagement.Enabled. Duende 8 manages signing keys automatically by default. This example makes that a switch because of what we found migrating: automatic key management is an edition-gated feature, and a stock IdentityServer4 app migrated as-is turns it on without anyone asking. Thelegacymode signs with the old IdentityServer4 key instead — which is also what keeps tokens valid across a cutover and rollback. The details are in what actually breaks in an IdentityServer4 → Duende migration and in the checklist’s licensing section.AddTestUsers. Quickstart convenience. In a real deployment this is ASP.NET Identity or your own user store.
2. Clients and scopes: Config.cs
From after/src/IdentityServer/Config.cs:
public static IEnumerable<ApiScope> ApiScopes =>
new List<ApiScope> { new ApiScope("api1", "My API") };
public static IEnumerable<Client> Clients =>
new List<Client>
{
// machine to machine client
new Client
{
ClientId = "client",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.ClientCredentials,
AllowedScopes = { "api1" }
},
// interactive ASP.NET Core MVC client
new Client
{
ClientId = "mvc",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.Code,
RedirectUris = { "https://localhost:5002/signin-oidc" },
PostLogoutRedirectUris = { "https://localhost:5002/signout-callback-oidc" },
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1"
}
}
};
This is the two client categories from the terms section, in code. client can only do client credentials and can only ask for api1. mvc does the authorization code flow, has registered redirect URIs (IdentityServer will refuse any other), and may ask for identity scopes plus api1. On first run the quickstart seeds these into the configuration store; after that, the database is the source of truth and Config.cs is just the seed.
3. The interactive client: MvcClient
From after/src/MvcClient/Startup.cs:
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "https://localhost:5001";
options.ClientId = "mvc";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.Scope.Add("api1");
options.SaveTokens = true;
});
There is no Duende-specific code here — it is the standard ASP.NET Core OpenID Connect handler pointed at the server. Two details that matter in practice: DefaultMapInboundClaims = false stops the handler renaming standard JWT claims to legacy Microsoft claim types, and SaveTokens = true keeps the access token so the app can call the API. One behaviour worth knowing: on .NET 10 this handler automatically uses Pushed Authorization Requests when the server advertises them, which Duende does. Nothing in this file asked for it.
4. The API: accept the token, require the scope
From after/src/Api/Startup.cs:
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "https://localhost:5001";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
});
services.AddAuthorization(options =>
{
options.AddPolicy("ApiScope", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim("scope", "api1");
});
});
The API trusts tokens from the authority by fetching its discovery document and signing keys — which is exactly why the signing-key decision in step 1 matters: if the server starts signing with a key the API has never seen, this is where requests start failing with 401. The ApiScope policy is what turns “a valid token” into “a token that is allowed to call this API”.
What this example does not cover
- Real users.
AddTestUsersis a stand-in for ASP.NET Identity or your own store. - Production key management. The example deliberately exposes the choice; it doesn’t make it for you.
- A license. The repo runs without one, which Duende permits for development and testing. Production requires a license, and which edition depends on features like automatic key management — see the checklist’s licensing section for what we found.
Coming from IdentityServer4?
The before/ folder in the same repository is the untouched IdentityServer4 version of this exact example, and the migration between them is logged step by step with timestamps. If you are planning that move, read what actually breaks in an IdentityServer4 → Duende migration first, then work through the migration checklist. If you’d rather have it done, that is the IdentityServer migration sprint.
Related Posts
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.
OpenID Connect (OIDC) in .NET Core
Learn how OpenID Connect works, how it differs from OAuth 2.0 and SAML, and explore .NET Core frameworks like IdentityServer4 and Duende IdentityServer for implementing OIDC authentication.
Mastering JWT in .NET Core: Encoding and Decoding
A comprehensive guide to JSON Web Tokens in .NET Core covering JWT claims, encoding, decoding, certificate-based signing, algorithm comparison, and security best practices.