Skip to content
Introducing the next era of Duende IdentityServer. Read our CEO’s announcement

Duende IdentityServer v7.4 to v8.0

IdentityServer v8.0 includes support for .NET 10, SAML 2.0 Identity Provider support, conformance reporting, and many other fixes and enhancements.

IdentityServer 8.0 targets .NET 10 only. In your IdentityServer host project, update the target framework to .NET 10 (net10.0):

.csproj
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>

or

.csproj
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>

Any NuGet packages you use that target an older version of .NET should also be updated. For example, Microsoft.EntityFrameworkCore.SqlServer or Microsoft.AspNetCore.Authentication.Google should be updated to their .NET 10-compatible versions. Depending on your IdentityServer host project, there may or may not be code changes from those updated dependencies.

In your IdentityServer host project, update the version of the Duende IdentityServer package.

.csproj
<PackageReference Include="Duende.IdentityServer" Version="7.4.7" />
<PackageReference Include="Duende.IdentityServer" Version="8.0.2"/>

If you use any of the other Duende.IdentityServer packages, update those as well:

<PackageReference Include="Duende.IdentityServer.EntityFramework" Version="8.0.2"/>
<PackageReference Include="Duende.IdentityServer.AspNetIdentity" Version="8.0.2"/>
<PackageReference Include="Duende.IdentityServer.Configuration" Version="8.0.2"/>

If you are using the Entity Framework storage packages, new tables are required to store SAML Service Provider configurations. Run the following migration commands to update your database schema:

Terminal
dotnet ef migrations add Update_DuendeIdentityServer_v8_0 -c ConfigurationDbContext -o Migrations/ConfigurationDb
dotnet ef database update -c ConfigurationDbContext

The migration creates several new tables in the configuration database: SamlServiceProviders, SamlAllowedScopes, SamlAssertionConsumerServices, SamlAuthnContextMappings, SamlCertificates, SamlClaimMappings, and SamlRequestedClaimTypes.

SQL Server database objects created by this migration
CREATE TABLE [SamlServiceProviders] (
[Id] int NOT NULL IDENTITY,
[EntityId] nvarchar(200) NOT NULL,
[DisplayName] nvarchar(200) NULL,
[Description] nvarchar(1000) NULL,
[Enabled] bit NOT NULL,
[ClockSkewSeconds] float NULL,
[RequestMaxAgeSeconds] float NULL,
[AssertionLifetimeSeconds] float NULL,
[SingleLogoutServiceLocation] nvarchar(400) NULL,
[SingleLogoutServiceBinding] int NULL,
[RequireSignedAuthnRequests] bit NULL,
[RequireSignedLogoutResponses] bit NULL,
[AllowIdpInitiated] bit NOT NULL,
[DefaultNameIdFormat] nvarchar(2000) NULL,
[EmailNameIdClaimType] nvarchar(200) NULL,
[SigningBehavior] int NULL,
[AllowedSignatureAlgorithms] nvarchar(max) NULL,
[Created] datetime2 NOT NULL,
[Updated] datetime2 NULL,
[LastAccessed] datetime2 NULL,
[NonEditable] bit NOT NULL,
CONSTRAINT [PK_SamlServiceProviders] PRIMARY KEY ([Id])
);
GO
CREATE TABLE [SamlAllowedScopes] (
[Id] int NOT NULL IDENTITY,
[Scope] nvarchar(200) NOT NULL,
[SamlServiceProviderId] int NOT NULL,
CONSTRAINT [PK_SamlAllowedScopes] PRIMARY KEY ([Id]),
CONSTRAINT [FK_SamlAllowedScopes_SamlServiceProviders_SamlServiceProviderId] FOREIGN KEY ([SamlServiceProviderId]) REFERENCES [SamlServiceProviders] ([Id]) ON DELETE CASCADE
);
GO
CREATE TABLE [SamlAssertionConsumerServices] (
[Id] int NOT NULL IDENTITY,
[Location] nvarchar(400) NOT NULL,
[Binding] nvarchar(200) NOT NULL,
[Index] int NOT NULL,
[IsDefault] bit NOT NULL,
[SamlServiceProviderId] int NOT NULL,
CONSTRAINT [PK_SamlAssertionConsumerServices] PRIMARY KEY ([Id]),
CONSTRAINT [FK_SamlAssertionConsumerServices_SamlServiceProviders_SamlServiceProviderId] FOREIGN KEY ([SamlServiceProviderId]) REFERENCES [SamlServiceProviders] ([Id]) ON DELETE CASCADE
);
GO
CREATE TABLE [SamlAuthnContextMappings] (
[Id] int NOT NULL IDENTITY,
[OidcValue] nvarchar(250) NOT NULL,
[SamlAuthnContextClassRef] nvarchar(500) NOT NULL,
[SamlServiceProviderId] int NOT NULL,
CONSTRAINT [PK_SamlAuthnContextMappings] PRIMARY KEY ([Id]),
CONSTRAINT [FK_SamlAuthnContextMappings_SamlServiceProviders_SamlServiceProviderId] FOREIGN KEY ([SamlServiceProviderId]) REFERENCES [SamlServiceProviders] ([Id]) ON DELETE CASCADE
);
GO
CREATE TABLE [SamlCertificates] (
[Id] int NOT NULL IDENTITY,
[Data] nvarchar(4000) NOT NULL,
[Use] int NOT NULL,
[SamlServiceProviderId] int NOT NULL,
CONSTRAINT [PK_SamlCertificates] PRIMARY KEY ([Id]),
CONSTRAINT [FK_SamlCertificates_SamlServiceProviders_SamlServiceProviderId] FOREIGN KEY ([SamlServiceProviderId]) REFERENCES [SamlServiceProviders] ([Id]) ON DELETE CASCADE
);
GO
CREATE TABLE [SamlClaimMappings] (
[Id] int NOT NULL IDENTITY,
[ClaimType] nvarchar(250) NOT NULL,
[SamlAttributeName] nvarchar(250) NOT NULL,
[SamlServiceProviderId] int NOT NULL,
CONSTRAINT [PK_SamlClaimMappings] PRIMARY KEY ([Id]),
CONSTRAINT [FK_SamlClaimMappings_SamlServiceProviders_SamlServiceProviderId] FOREIGN KEY ([SamlServiceProviderId]) REFERENCES [SamlServiceProviders] ([Id]) ON DELETE CASCADE
);
GO
CREATE TABLE [SamlRequestedClaimTypes] (
[Id] int NOT NULL IDENTITY,
[ClaimType] nvarchar(250) NOT NULL,
[SamlServiceProviderId] int NOT NULL,
CONSTRAINT [PK_SamlRequestedClaimTypes] PRIMARY KEY ([Id]),
CONSTRAINT [FK_SamlRequestedClaimTypes_SamlServiceProviders_SamlServiceProviderId] FOREIGN KEY ([SamlServiceProviderId]) REFERENCES [SamlServiceProviders] ([Id]) ON DELETE CASCADE
);
GO
CREATE UNIQUE INDEX [IX_SamlAllowedScopes_SamlServiceProviderId_Scope] ON [SamlAllowedScopes] ([SamlServiceProviderId], [Scope]);
GO
CREATE UNIQUE INDEX [IX_SamlAssertionConsumerServices_SamlServiceProviderId_Location] ON [SamlAssertionConsumerServices] ([SamlServiceProviderId], [Location]);
GO
CREATE UNIQUE INDEX [IX_SamlAuthnContextMappings_SamlServiceProviderId_OidcValue] ON [SamlAuthnContextMappings] ([SamlServiceProviderId], [OidcValue]);
GO
CREATE INDEX [IX_SamlCertificates_SamlServiceProviderId] ON [SamlCertificates] ([SamlServiceProviderId]);
GO
CREATE UNIQUE INDEX [IX_SamlClaimMappings_SamlServiceProviderId_ClaimType] ON [SamlClaimMappings] ([SamlServiceProviderId], [ClaimType]);
GO
CREATE UNIQUE INDEX [IX_SamlRequestedClaimTypes_SamlServiceProviderId_ClaimType] ON [SamlRequestedClaimTypes] ([SamlServiceProviderId], [ClaimType]);
GO
CREATE UNIQUE INDEX [IX_SamlServiceProviders_EntityId] ON [SamlServiceProviders] ([EntityId]);
GO

If you are using the operational store from the Entity Framework storage packages, you need to migrate the operational database. This migration adds tables that track logout session state during Single Logout (SLO) flows and persist sign-in request state while the user authenticates.

Run the following commands to apply the operational store migration:

Terminal
dotnet ef migrations add Update_DuendeIdentityServer_v8_0_Saml -c PersistedGrantDbContext -o Migrations/PersistedGrantDb
dotnet ef database update -c PersistedGrantDbContext

The migration creates three new tables in the operational database: SamlLogoutSessions, SamlLogoutSessionRequestIndices, and SamlSigninStates.

SQL Server database objects created by this migration
CREATE TABLE [SamlLogoutSessions] (
[Id] bigint NOT NULL IDENTITY,
[LogoutId] nvarchar(200) NOT NULL,
[SerializedSession] nvarchar(max) NOT NULL,
[ExpiresAtUtc] datetime2 NOT NULL,
[Version] bigint NOT NULL,
CONSTRAINT [PK_SamlLogoutSessions] PRIMARY KEY ([Id])
);
GO
CREATE TABLE [SamlSigninStates] (
[Id] bigint NOT NULL IDENTITY,
[StateId] uniqueidentifier NOT NULL,
[SerializedState] nvarchar(max) NOT NULL,
[ExpiresAtUtc] datetime2 NOT NULL,
[ServiceProviderEntityId] nvarchar(200) NOT NULL,
CONSTRAINT [PK_SamlSigninStates] PRIMARY KEY ([Id])
);
GO
CREATE TABLE [SamlLogoutSessionRequestIndices] (
[Id] bigint NOT NULL IDENTITY,
[RequestId] nvarchar(200) NOT NULL,
[SamlLogoutSessionId] bigint NOT NULL,
CONSTRAINT [PK_SamlLogoutSessionRequestIndices] PRIMARY KEY ([Id]),
CONSTRAINT [FK_SamlLogoutSessionRequestIndices_SamlLogoutSessions_SamlLogoutSessionId] FOREIGN KEY ([SamlLogoutSessionId]) REFERENCES [SamlLogoutSessions] ([Id]) ON DELETE CASCADE
);
GO
CREATE UNIQUE INDEX [IX_SamlLogoutSessionRequestIndices_RequestId] ON [SamlLogoutSessionRequestIndices] ([RequestId]);
GO
CREATE INDEX [IX_SamlLogoutSessionRequestIndices_SamlLogoutSessionId] ON [SamlLogoutSessionRequestIndices] ([SamlLogoutSessionId]);
GO
CREATE INDEX [IX_SamlLogoutSessions_ExpiresAtUtc] ON [SamlLogoutSessions] ([ExpiresAtUtc]);
GO
CREATE UNIQUE INDEX [IX_SamlLogoutSessions_LogoutId] ON [SamlLogoutSessions] ([LogoutId]);
GO
CREATE INDEX [IX_SamlSigninStates_ExpiresAtUtc] ON [SamlSigninStates] ([ExpiresAtUtc]);
GO
CREATE UNIQUE INDEX [IX_SamlSigninStates_StateId] ON [SamlSigninStates] ([StateId]);
GO

If your IdentityServer implementation uses a custom IClientStore, you must add the new GetAllClientsAsync method (see Breaking Change below).

If you are using Duende.IdentityServer.EntityFramework, the package migration will handle schema updates automatically when you run the EF migrations command above.

ICache<T> / DefaultCache<T> Replaced by HybridCache

Section titled “ICache<T> / DefaultCache<T> Replaced by HybridCache”

The custom ICache<T> interface and its default implementation DefaultCache<T> have been removed. Configuration-store caching now uses Microsoft’s HybridCache from the Microsoft.Extensions.Caching.Hybrid package.

If you use the built-in caching (most applications): No code changes are needed. The AddInMemoryCaching() and AddXxxStoreCache<T>() extension methods continue to work as before.

If you had a custom ICache<T> implementation: You can no longer register a custom ICache<T>. Instead, customize the keyed HybridCache instance registered under ServiceProviderKeys.ConfigurationStoreCache. For example, to add a distributed cache backend:

Program.cs
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost:6379";
});
builder.Services.AddIdentityServer()
.AddInMemoryCaching() // registers the keyed HybridCache
.AddClientStoreCache<YourCustomClientStore>();

HybridCache automatically uses any registered IDistributedCache as its L2 cache tier.

CachingOptions.CacheLockTimeout is now obsolete. HybridCache provides built-in stampede protection, so the explicit lock timeout is no longer used for configuration-store caching. The property is retained (marked [Obsolete]) because it is still used internally by KeyManager for key-management locking.

Duende.IdentityServer.IClock has been removed. Replace it with the standard .NET System.TimeProvider (available since .NET 8).

MyService.cs
// Before (v7.x)
public class MyService
{
public MyService(IClock clock) { }
}
// After (v8.0)
public class MyService
{
public MyService(TimeProvider timeProvider) { }
}

To get the current time, use timeProvider.GetUtcNow() instead of clock.UtcNow.

Internal IdentityServer services such as DefaultTokenCreationService and DefaultTokenService have been updated to accept TimeProvider in place of IClock.

CancellationToken Now Required on All Interface Methods

Section titled “CancellationToken Now Required on All Interface Methods”

All store and service interfaces now include a CancellationToken parameter on every async method. The parameter name is ct (not cancellationToken).

MyClientStore.cs
// Before (v7.x)
public class MyClientStore : IClientStore
{
public Task<Client?> FindClientByIdAsync(string clientId)
{
// ...
}
}
// After (v8.0)
public class MyClientStore : IClientStore
{
public Task<Client?> FindClientByIdAsync(string clientId, CancellationToken ct)
{
// ...
}
public IAsyncEnumerable<Client> GetAllClientsAsync(CancellationToken ct)
{
// ...
}
}

This applies to all store interfaces in Duende.IdentityServer.Stores and service interfaces in Duende.IdentityServer.Services. Update all custom implementations to add the CancellationToken ct parameter.

When consuming these methods from a web context, for example in an ASP.NET MVC Controller or Razor Pages, use HttpContext.RequestAborted as the CancellationToken so that executuion is halted when the current HTTP request is aborted.

ITokenValidator.ValidateAccessTokenAsync Signature Changed

Section titled “ITokenValidator.ValidateAccessTokenAsync Signature Changed”

ITokenValidator.ValidateAccessTokenAsync now requires an expectedScope parameter (pass null if you don’t need to restrict by scope) and a CancellationToken:

var result = await _validator.ValidateAccessTokenAsync(token);
var result = await _validator.ValidateAccessTokenAsync(token, expectedScope: null, cancellationToken);

If you call this method in a custom extension grant validator, the CancellationToken is available from the ValidateAsync method signature.

ICancellationTokenProvider has been removed from both Duende.IdentityServer and Duende.IdentityServer.Configuration.EntityFramework. If you injected this service, remove that dependency. Use the CancellationToken passed directly to interface methods instead.

IdentityServer now always uses HTTP 303 (See Other) for redirects from POST endpoints, in compliance with FAPI 2.0 Section 5.3.2.2.

IClientStore.GetAllClientsAsync Now Required

Section titled “IClientStore.GetAllClientsAsync Now Required”

IClientStore now includes a second required method:

IAsyncEnumerable<Client> GetAllClientsAsync(CancellationToken ct);

All custom IClientStore implementations must add this method. It is used by the Financial-Grade Security and Conformance report and configuration validation features. Return an async enumerable of all configured clients.

For the in-memory store, this returns all clients passed to AddInMemoryClients. For Entity Framework, it queries the Clients table. Custom implementations should return all clients without filtering.

IRefreshTokenService Method Signatures Changed

Section titled “IRefreshTokenService Method Signatures Changed”

The CreateRefreshTokenAsync and UpdateRefreshTokenAsync methods on IRefreshTokenService now accept dedicated request objects instead of individual parameters:

IRefreshTokenService.cs
// Before (v7.x)
public interface IRefreshTokenService
{
Task<TokenValidationResult> ValidateRefreshTokenAsync(string token, Client client);
Task<string> CreateRefreshTokenAsync(ClaimsPrincipal subject, Token accessToken, Client client);
Task<string> UpdateRefreshTokenAsync(string handle, RefreshToken refreshToken, Client client);
}
// After (v8.0)
public interface IRefreshTokenService
{
Task<TokenValidationResult> ValidateRefreshTokenAsync(string token, Client client, CancellationToken ct);
Task<string> CreateRefreshTokenAsync(RefreshTokenCreationRequest request, CancellationToken ct);
Task<string> UpdateRefreshTokenAsync(RefreshTokenUpdateRequest request, CancellationToken ct);
}

The new RefreshTokenCreationRequest and RefreshTokenUpdateRequest types carry the same data as the old individual parameters. If you have a custom implementation or subclass of DefaultRefreshTokenService, update the method signatures and use the request objects to access the data previously passed as separate arguments.

See the Refresh Token Service reference for the updated interface documentation.

Nullable Reference Types Enabled Across All Assemblies

Section titled “Nullable Reference Types Enabled Across All Assemblies”

Nullable reference types (NRT) are now enabled across all IdentityServer assemblies. Method return types and parameters now carry explicit nullability annotations:

// Before (v7.x) - return type was non-nullable (implicitly nullable)
Task<Client> FindClientByIdAsync(string clientId);
// After (v8.0) - explicitly nullable
Task<Client?> FindClientByIdAsync(string clientId, CancellationToken ct);

If you have custom implementations of IdentityServer interfaces, you may see new compiler warnings about null handling after upgrading. Update your code to handle nullable return values appropriately. The #nullable enable directive is now the default for all projects.

response_mode Validated Earlier in the Authorization Pipeline

Section titled “response_mode Validated Earlier in the Authorization Pipeline”

The response_mode parameter is now parsed and validated before grant-type and PKCE checks in the authorization endpoint pipeline. As a result, error responses generated during grant-type or PKCE validation will now be delivered using the client’s requested response_mode (for example, fragment or form_post) rather than the default mode.

Most applications are unaffected by this change. It only impacts scenarios where code relied on the specific shape of error responses from the authorize endpoint when an invalid response_mode was combined with other validation failures.

Two DPoP type names contained a spelling error (Validaton instead of Validation). They have been renamed:

Before (v7.x)After (v8.0)
DPoPProofValidatonContextDPoPProofValidationContext
DPoPProofValidatonResultDPoPProofValidationResult

If your code references either of the old names, do a find-and-replace to update them.

AuthorizationError Renamed to InteractionError

Section titled “AuthorizationError Renamed to InteractionError”

The AuthorizationError enum has been renamed to InteractionError. Update any references in your code:

// Before (v7.x)
if (result.Error == AuthorizationError.LoginRequired) { ... }
// After (v8.0)
if (result.Error == InteractionError.LoginRequired) { ... }

New Interface Implementations on Request Types

Section titled “New Interface Implementations on Request Types”

AuthorizationRequest now implements IAuthenticationContext and ValidatedRequest now implements IValidatedRequest. These are additive changes that should not break existing code, but if you have custom types that shadow these interfaces or use explicit interface implementations that conflict, you may need to update them.

PersistedGrantFilter Collection Properties Are Now Non-Nullable

Section titled “PersistedGrantFilter Collection Properties Are Now Non-Nullable”

The ClientIds and Types properties on PersistedGrantFilter changed from nullable IEnumerable<string>? to non-nullable IReadOnlyCollection<string>, defaulting to empty collections:

PersistedGrantFilter.cs
// Before (v7.x)
public class PersistedGrantFilter
{
public IEnumerable<string> ClientIds { get; set; } // nullable, null by default
public IEnumerable<string> Types { get; set; } // nullable, null by default
}
// After (v8.0)
public class PersistedGrantFilter
{
public IReadOnlyCollection<string> ClientIds { get; set; } = []; // non-nullable, empty by default
public IReadOnlyCollection<string> Types { get; set; } = []; // non-nullable, empty by default
}

Replace null checks with count checks:

// Before (v7.x)
if (filter.ClientIds != null) { ... }
// After (v8.0)
if (filter.ClientIds.Count > 0) { ... }

IEnumerable<T> Return Types Changed to IReadOnlyCollection<T>

Section titled “IEnumerable<T> Return Types Changed to IReadOnlyCollection<T>”

Several interface methods that previously returned Task<IEnumerable<T>> now return Task<IReadOnlyCollection<T>>. The change affects these interfaces:

  • IResourceStoreFindIdentityResourcesByScopeNameAsync, FindApiScopesByNameAsync, FindApiResourcesByScopeNameAsync, FindApiResourcesByNameAsync
  • IPersistedGrantStoreGetAllAsync
  • IPersistedGrantServiceGetAllGrantsAsync
  • IBackChannelAuthenticationRequestStoreGetLoginsForUserAsync
  • ISigningKeyStoreLoadKeysAsync
  • IIdentityProviderStoreGetAllSchemeNamesAsync
  • IIdentityServerInteractionServiceGetAllUserGrantsAsync
  • IBackchannelAuthenticationInteractionServiceGetPendingLoginRequestsForCurrentUserAsync
  • IUserSessionGetClientListAsync, GetSamlSessionListAsync
// Before (v7.x)
Task<IEnumerable<IdentityResource>> FindIdentityResourcesByScopeNameAsync(
IEnumerable<string> scopeNames, CancellationToken ct);
// After (v8.0)
Task<IReadOnlyCollection<IdentityResource>> FindIdentityResourcesByScopeNameAsync(
IEnumerable<string> scopeNames, CancellationToken ct);

IReadOnlyCollection<T> implements IEnumerable<T>, so callers that only enumerate the results are unaffected. Custom implementations of these interfaces must update their return types.

The Client.DPoPValidationMode property (of type DPoPTokenExpirationValidationMode) is unchanged in IdentityServer’s client model.

In the JwtBearer package (Duende.AspNetCore.Authentication.JwtBearer), the DPoP configuration was restructured for v8.0:

  • New enum DPoPProofExpirationMode with values IssuedAt, Nonce, and Both
  • DPoPOptions.ProofTokenExpirationMode property (default: IssuedAt)
  • New properties: ProofTokenIssuedAtClockSkew, ProofTokenNonceClockSkew, EnableReplayDetection
  • New interface IDPoPNonceValidator with default implementation DefaultDPoPNonceValidator
  • DPoPExtensions class replaced by DPoPServiceCollectionExtensions

Update any code that references DPoPExtensions to use DPoPServiceCollectionExtensions instead.

IAuthorizationParametersMessageStore Removed

Section titled “IAuthorizationParametersMessageStore Removed”

IAuthorizationParametersMessageStore and its implementations (DistributedCacheAuthorizationParametersMessageStore, QueryStringAuthorizationParametersMessageStore) have been removed. This interface was deprecated in v7.0 in favor of Pushed Authorization Requests (PAR).

The AddAuthorizationParametersMessageStore<T>() builder extension method has also been removed.

If you used DistributedCacheAuthorizationParametersMessageStore via Duende.IdentityServer.EntityFramework: No action needed. The store was auto-registered and its removal is transparent. Authorization parameters now travel via the query string or via PAR’s request_uri.

If you registered a custom IAuthorizationParametersMessageStore: Remove the registration. If you need to avoid passing authorization parameters on the query string, use PAR instead.

Program.cs
// Before (v7.x) - remove this
builder.Services.AddIdentityServer()
.AddAuthorizationParametersMessageStore<MyCustomStore>();
// After (v8.0) - if you need server-side parameter storage, use PAR instead
builder.Services.AddIdentityServer(options =>
options.PushedAuthorization.Required = true
);

If you subclassed AuthorizeInteractionPageHttpWriter: The constructor no longer accepts an optional IAuthorizationParametersMessageStore parameter, and the protected AuthorizationParametersMessageStore property has been removed. Update your subclass constructor to remove the store parameter.

IdentityProviderStore Constructor Changed (EntityFramework.Storage)

Section titled “IdentityProviderStore Constructor Changed (EntityFramework.Storage)”

The IdentityProviderStore in Duende.IdentityServer.EntityFramework.Storage now requires an IIdentityProviderFactory parameter. This factory allows the store to construct the correct derived IdentityProvider type (for example, OidcProvider, SamlProvider, or custom types) when loading from the database.

If you subclass IdentityProviderStore, update your constructor:

// Before (v7.x)
public MyIdentityProviderStore(
IConfigurationDbContext context,
ILogger<IdentityProviderStore> logger)
: base(context, logger) { }
// After (v8.0)
public MyIdentityProviderStore(
IConfigurationDbContext context,
ILogger<IdentityProviderStore> logger,
IIdentityProviderFactory identityProviderFactory)
: base(context, logger, identityProviderFactory) { }

IIdentityProviderFactory is automatically registered in the service provider by IdentityServer’s AddIdentityServer() call.

IUserSession: New SAML Session Methods (Breaking for Custom Implementations)

Section titled “IUserSession: New SAML Session Methods (Breaking for Custom Implementations)”

Three new methods were added to IUserSession to support SAML 2.0 session tracking:

Task AddSamlSessionAsync(SamlSpSessionData session, CancellationToken ct);
Task<IReadOnlyCollection<SamlSpSessionData>> GetSamlSessionListAsync(CancellationToken ct);
Task RemoveSamlSessionAsync(string entityId, CancellationToken ct);

If you have a custom IUserSession implementation, you must add these three methods. If you do not use SAML 2.0, the methods can have minimal implementations:

public Task AddSamlSessionAsync(SamlSpSessionData session, CancellationToken ct) => Task.CompletedTask;
public Task<IReadOnlyCollection<SamlSpSessionData>> GetSamlSessionListAsync(CancellationToken ct)
=> Task.FromResult<IReadOnlyCollection<SamlSpSessionData>>([]);
public Task RemoveSamlSessionAsync(string entityId, CancellationToken ct) => Task.CompletedTask;

See the User Session Service reference for the full updated interface.

ProfileDataRequestContext.Client Renamed to ProfileDataRequestContext.Application

Section titled “ProfileDataRequestContext.Client Renamed to ProfileDataRequestContext.Application”

The ProfileDataRequestContext.Client property has been renamed to ProfileDataRequestContext.Application.

AuthorizationError Renamed to InteractionError

Section titled “AuthorizationError Renamed to InteractionError”

The AuthorizationError enum has been renamed to InteractionError, and DenyAuthorizationAsync has been renamed to DenyAuthenticationAsync. The new method also accepts an IAuthenticationContext (protocol-agnostic) instead of an AuthorizationRequest, so it works for both OpenID Connect (OIDC) and SAML flows.

If you call DenyAuthorizationAsync in your login or consent pages, update to the new names:

*.cs
await _interaction.DenyAuthorizationAsync(context, AuthorizationError.AccessDenied);
await _interaction.DenyAuthenticationAsync(context, InteractionError.AccessDenied);

The InteractionError enum values remain the same (AccessDenied, LoginRequired, InteractionRequired).

The IdentityServerLicense class (and its base class Duende.License) no longer exists in v8. If you injected IdentityServerLicense directly, you need to switch to LicenseInformation from the Duende.IdentityServer.Licensing namespace.

LicenseInformation is registered automatically as a singleton, so you can inject it the same way. Use license.IsConfigured to check whether a license is present instead of a null check.

// Before (v7.x)
using Duende.IdentityServer;
public class MyPage : PageModel
{
private readonly IdentityServerLicense? _license;
public MyPage(IdentityServerLicense? license = null)
{
_license = license;
}
public void OnGet()
{
if (_license != null)
{
// licensed behavior
}
}
}
// After (v8.0)
using Duende.IdentityServer.Licensing;
public class MyPage : PageModel
{
private readonly LicenseInformation _license;
public MyPage(LicenseInformation license)
{
_license = license;
}
public void OnGet()
{
if (_license.IsConfigured)
{
// licensed behavior
}
}
}

LicenseUsageSummary.LicenseEdition Replaced by EntitledSkus

Section titled “LicenseUsageSummary.LicenseEdition Replaced by EntitledSkus”

The LicenseEdition string property on LicenseUsageSummary has been removed. It is replaced by EntitledSkus, which is an IReadOnlyCollection<string> listing the SKUs the license covers.

// Before (v7.x)
string edition = summary.LicenseEdition;
// After (v8.0)
IReadOnlyCollection<string> skus = summary.EntitledSkus;

If you previously displayed or compared the edition string, iterate over EntitledSkus instead. See the LicenseUsageSummary reference for the full updated model.

IdentityServer 8.0 adds support for acting as a SAML 2.0 Identity Provider, enabling integration with enterprise applications and legacy systems that use SAML. See the SAML 2.0 documentation for setup and configuration details.

Financial-Grade Security and Conformance Report

Section titled “Financial-Grade Security and Conformance Report”

A new Duende.IdentityServer.ConformanceReport package generates an HTML report assessing your IdentityServer deployment against OAuth 2.1 and FAPI 2.0 specifications. See the Financial-Grade Security and Conformance Report documentation for details.

A new AddDynamicProvider<THandler, TProviderOptions, TProvider, TConfigureOptions> builder extension method simplifies registering custom dynamic identity provider types. It consolidates provider type mapping, configure options, and handler service provider registration into a single call. See the dynamic providers documentation for details.

IdentityServer now reads the license key from IConfiguration automatically, so you no longer need to set it in code. If LicenseKey is not set in your AddIdentityServer call, IdentityServer checks the following configuration keys in order, using the first non-empty value it finds:

  1. Duende:IdentityServer:LicenseKey
  2. Duende:LicenseKey

Whitespace is trimmed, and empty or whitespace-only values are ignored.

You can add the license key to appsettings.json:

appsettings.json
{
"Duende": {
"IdentityServer": {
"LicenseKey": "eyJhbG..."
}
}
}

Or use the shorter key:

appsettings.json
{
"Duende": {
"LicenseKey": "eyJhbG..."
}
}

Because IConfiguration supports many providers, you can also supply the key via environment variables (for example, Duende__IdentityServer__LicenseKey or Duende__LicenseKey), Azure App Configuration, Azure Key Vault, or any other configuration source without changing your startup code.

Section titled “Device Flow Consent Is No Longer Remembered”

The device flow consent UI no longer offers a “Remember My Decision” option. ConsentResponse.RememberConsent is now always set to false during device flow authorization.

This change follows RFC 8628 security guidance: because the device initiating the flow is different from the device where the user authenticates, persisting consent creates a cross-device phishing risk.

If you use the Duende UI templates for device flow, this change is already applied. If you maintain a custom device flow consent page, remove the “remember consent” checkbox and ensure ConsentResponse.RememberConsent is set to false when calling IDeviceFlowInteractionService.HandleRequestAsync.

AuthorizeInteractionPageHttpWriter Is Now Public and Extensible

Section titled “AuthorizeInteractionPageHttpWriter Is Now Public and Extensible”

The class that builds and writes redirect responses to login, consent, create-account, and custom interaction pages (AuthorizeInteractionPageHttpWriter) is now public and designed to be subclassed. You can override its three virtual methods to customize how the return URL and redirect URL are constructed, or to add cookies and headers before the redirect is sent.

Register a subclass using AddHttpWriter<AuthorizeInteractionPageResult, YourWriter>():

Program.cs
builder.Services.AddIdentityServer()
.AddHttpWriter<AuthorizeInteractionPageResult, YourCustomWriter>();

See Customizing authorize interaction redirects for a full how-to guide.

When no IOperationalStoreNotification is registered, the token cleanup service now uses a single server-side ExecuteDeleteAsync call instead of a SELECT + DELETE roundtrip. This skips entity materialization and significantly reduces database load in high-volume deployments. No action is required, but you may notice reduced database activity during cleanup cycles.

Orphaned Grants Revoked on Session Overwrite

Section titled “Orphaned Grants Revoked on Session Overwrite”

When server-side sessions are enabled and a session cookie is reused by a different user (for example, after the original user’s session expires but the cookie key is reassigned), IdentityServer now automatically revokes the grants that belonged to the previous session. The grants revoked are:

  • refresh tokens
  • reference tokens
  • authorization codes
  • backchannel authentication requests

This prevents tokens from a previous user’s session from remaining valid after the session is overwritten.

Impact: If your application relies on grants surviving session overwrites (uncommon), this is a behavioral change. In most deployments, this is a security improvement that requires no action.

Secret Validator Log Level Changed from Error to Debug

Section titled “Secret Validator Log Level Changed from Error to Debug”

In ApiSecretValidator and ClientSecretValidator, “not found” log entries (for example, “No API secret found”, “No client with id ’…’ found”) have been downgraded from Error to Debug. These outcomes are expected during the introspection endpoint’s fallback authentication pattern and are not genuine errors.

To compensate, Warning-level log entries have been added at the token endpoint, token revocation endpoint, and backchannel authentication endpoint when client validation fails, so genuine authentication failures remain visible to operators.

Impact: If your monitoring or alerting watches for Error-level log entries from ApiSecretValidator or ClientSecretValidator to detect authentication failures, those alerts will no longer trigger. Update your alerting to watch for Warning-level entries at the endpoint level instead.

PreviewFeatureOptions Removed; Options Relocated

Section titled “PreviewFeatureOptions Removed; Options Relocated”

The PreviewFeatureOptions class and the IdentityServerOptions.Preview property have been removed. The options that were on Preview have moved to their permanent homes:

Old locationNew location
options.Preview.EnableDiscoveryDocumentCacheoptions.Discovery.EnableDiscoveryDocumentCache
options.Preview.DiscoveryDocumentCacheDurationoptions.Discovery.DiscoveryDocumentCacheDuration
options.Preview.StrictClientAssertionAudienceValidationoptions.StrictClientAssertionAudienceValidation

In addition, the default value of StrictClientAssertionAudienceValidation has changed. When the option lived under Preview, it defaulted to true. Now that it is a stable option on IdentityServerOptions, it defaults to false to avoid a breaking change for deployments that never opted into the preview.

If you previously set StrictClientAssertionAudienceValidation to true in the preview options, update your configuration to set it on IdentityServerOptions directly:

Program.cs
builder.Services.AddIdentityServer(options =>
{
options.Preview.StrictClientAssertionAudienceValidation = true;
options.StrictClientAssertionAudienceValidation = true;
});

Impact: Any code that references IdentityServerOptions.Preview or PreviewFeatureOptions will fail to compile. Update the property paths as shown in the table above. If you relied on StrictClientAssertionAudienceValidation being true by default (because you had it set under Preview), you must explicitly set it to true on the new location, or strict audience validation will be disabled.

Audience Validation Now Accepts Single-Element Arrays

Section titled “Audience Validation Now Accepts Single-Element Arrays”

When StrictClientAssertionAudienceValidation is enabled (or when a client assertion uses the client-authentication+jwt token type), the aud claim in private_key_jwt client assertions may now be either a plain string or a single-element JSON array containing the issuer identifier. Previously, only a plain string was accepted.

This aligns with draft-ietf-oauth-rfc7523bis, which permits the issuer identifier as the sole audience value in either form. Multi-element arrays are still rejected.

Impact: Clients that send the aud claim as a single-element array (for example, "aud": ["https://your-issuer"]) will now pass strict audience validation where they previously would have failed. No action is required unless you relied on the stricter behavior.

The license key format used in previous versions of IdentityServer is compatible with IdentityServer v8. No new subscription or purchase is necessary if you already have an active license. However, to make use of add-ons like SAML or Duende User Management, a new license key file will be required to run IdentityServer v8 in production.

Please contact our sales team to request an updated license file. Once you receive the updated license file, replace the configured LicenseKey value (for example in appsettings.json or an environment variable) with the new key content. See the licensing documentation for all supported configuration approaches.

Note that the new license key format is not backwards-compatible with previous versions of IdentityServer.

That’s it. Of course, at this point you can and should test that your IdentityServer is updated and working properly.